簡體   English   中英

TypeScript:構造函數中的位置或命名參數

[英]TypeScript: Positional OR Named parameters in constructor

我有一個 class 目前正在采用 7+ 位置參數。

class User {
  constructor (param1, param2, param3, …etc) {
    // …
  }
}

我想通過選項 object 將其轉換為命名參數。

type UserOptions = {
  param1: string
  // …
}

class User {
  constructor ({ param1, param2, param3, …etc } = {}: UserOptions) {
    // …
  }
}

這很好,除了現在需要重新設計很多測試來更改簽名,所以我想同時支持命名參數和位置參數。

我可以使代碼同時支持兩者,但我不確定如何在不寫出所有類型兩次的情況下將類型放入其中。 理想情況下,我的UserOptions中的類型列表將按照它們在UserOptions中定義的順序成為位置參數。

有沒有辦法做這樣的事情?

type UserOptions = {
  param1: string
  // …
}

type ToPositionalParameters<T> = [
  // ??? something like ...Object.values(T)
  // and somehow getting the keys as the positional argument names???
]

type PositionalUserOptions = ToPositionalParameters<UserOptions>

class User {
  constructor (...args: PositionalUserOptions);
  constructor (options: UserOptions);
  constructor (...args: [UserOptions] | PositionalUserOptions) { 
    // …
  }
}

還會考慮另一種解決方案,即命名的位置 arguments,也許這更容易?

這里有幾件事阻礙了你。

首先是 object 類型中的屬性順序目前在 TypeScript 中是不可觀察的,因為它們不會影響可分配性。 例如,類型系統中的類型{a: string, b: number}{b: number, a: string}沒有區別。 您可以執行一些骯臟的技巧來從編譯器中提取信息,以查看它是否將鍵表示為["a", "b"] vs ["b", "a"] ,但所做的只是給您一些編譯時排序; 當您從上到下閱讀類型聲明時,不能保證是有序的……哎呀,甚至不能保證每次編譯時都是相同的順序! 請參閱microsoft/TypeScript#17944以獲取一個未解決的問題,以要求有一致的順序。 請參閱microsoft/TypeScript#42178以獲取看似無關的代碼更改示例,該更改會改變預期的順序。 目前,我們不能以任何一致的方式自動將 object 類型轉換為有序元組。

第二個是 function 類型中的 arguments 的名稱故意不能用作字符串文字類型 它們是不可觀察的,原因與 object 屬性排序類似:它們不影響可分配性。 比如 function 類型(foo: string) => void(bar: string) => void在類型系統中沒有區別。 我什至認為沒有任何技巧可以暴露這些細節。 在類型系統中,function 參數名稱僅用作文檔或 IntelliSense。 您可以在命名的 function arguments 和標記的元組元素之間進行轉換,但僅此而已。 請參閱microsoft/TypeScript#28259 中的此注釋,說明我們不能將調用簽名參數名稱或元組標簽用作 TypeScript 中的字符串。 目前,我們不能自動將帶標簽的元組轉換為 object 類型,其中 object 的鍵對應於元組的標簽。


如果不是嘗試將 object 轉換為元組或將元組轉換為 object,您可以回避這兩個問題,而是提供足夠的信息來完成這兩個問題:

const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [string, number, boolean];

這里userOptionKeysUserOptions對象中所需鍵的顯式有序列表......與我們手動創建的PositionalUserOptions元組中的順序相同。 現在我們有了鍵名,我們可以構建UserOptions

type UserOptions = { [I in Exclude<keyof PositionalUserOptions, keyof any[]> as
  typeof userOptionKeys[I]]: PositionalUserOptions[I] }
/* type UserOptions = {
    param1: string;
    param2: number;
    thirdOne: boolean;
} */

當我們這樣做時,我們可以編寫一個 function 來將PositionalUserOptions類型的元組轉換為 UserOptions 類型的UserOptions (使用any類型的斷言來使編譯器不必嘗試驗證它,但它不能這樣做容易地):

function positionalToObj(opts: PositionalUserOptions): UserOptions {
  return opts.reduce((acc, v, i) => (acc[userOptionKeys[i]] = v, acc), {} as any)
}

現在我們可以編寫User class,在構造函數的實現中使用positionalToObj來規范化事物:

class User {
  constructor(...args: PositionalUserOptions);
  constructor(options: UserOptions);
  constructor(...args: [UserOptions] | PositionalUserOptions) {
    const opts = args.length === 1 ? args[0] : positionalToObj(args);
    console.log(opts);
  }
}

new User("a", 1, true);
/* {
  "param1": "a",
  "param2": 1,
  "thirdOne": true
}  */

從類型系統和可分配性的角度來看,它是有效的。 從文檔/智能感知的角度來看,這是您能做的最好的事情。 這不是很好。 當您調用new User()時,多參數版本的文檔將為您提供像args_0args_1這樣的標簽。 如果您希望它們是param1param2 ,您只需要硬着頭皮寫出兩次參數名稱; 一次作為字符串文字,再次作為元組標簽,因為沒有辦法將一個轉換為另一個:

const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [param1: string, param2: number, thirdOne: boolean];

這值得么? 也許……這取決於你。

Playground 代碼鏈接

可以這樣做,但是 TypeScript 團隊不鼓勵您必須使用的工具,並且依賴於某些內部類型的實例化順序(至少這是要求聯合到元組操作的問題所暗示的)。

盡管如此,弄清楚它還是很有趣的,所以這里是 go:

// Via https://github.com/Microsoft/TypeScript/issues/13298#issuecomment-707364842
type UnionToTuple<T> = (
    (
        (
            T extends any
                ? (t: T) => T
                : never
        ) extends infer U
            ? (U extends any
                ? (u: U) => any
                : never
            ) extends (v: infer V) => any
                ? V
                : never
            : never
    ) extends (_: any) => infer W
        ? [...UnionToTuple<Exclude<T, W>>, W]
        : []
);

type Head<T> = T extends [infer A, ...any] ? A : never
type Tail<T extends any[]> = T extends [any, ...infer A] ? A : never;

type PluckFieldTypes<T extends object, Fields extends any[] =
  UnionToTuple<keyof T>> = _PluckFieldType<T, Head<Fields>, Tail<Fields>, []>
type _PluckFieldType<
  T extends object,
  CurrentKey,
  RemainingKeys extends any[],
  Result extends any[]
  > = RemainingKeys['length'] extends 0
      ? CurrentKey extends keyof T ? [...Result, T[CurrentKey]] : never
      : CurrentKey extends keyof T
        /* && */? RemainingKeys extends (keyof T)[]
        ? [...Result, T[CurrentKey], ..._PluckFieldType<T, Head<RemainingKeys>, Tail<RemainingKeys>, []>]
        : never : never;

// -- IMPL --
type Args = {
  param1: string,
  param2: number,
  param3: Date,
  param4: number,
  param5: string,
  param6: Date,
  param7: boolean,
  param8: boolean,
  param9: null,
  param10: 'abc' | 'xyz'
}

class CompositeClass {
  constructor(params: Args);
  constructor(param1: string, param2: number, param3: Date);
  constructor(...args: [Args] | PluckFieldTypes<Args>) {
  }
}

...args的推斷類型最終是(parameter) args: [Args] | [string, number, Date, number, string, Date, boolean, boolean, null, "abc" | "xyz"] (parameter) args: [Args] | [string, number, Date, number, string, Date, boolean, boolean, null, "abc" | "xyz"] (parameter) args: [Args] | [string, number, Date, number, string, Date, boolean, boolean, null, "abc" | "xyz"]並且第二個構造函數在它下面有一個紅色的波浪線,直到你添加所有其他參數。

我們唯一不能得到的是要求 arguments 的名稱與字段的名稱匹配(但這是因為標記的元組不會使其標簽在類型級別上可訪問,因此您無法從內置ConstructorArguments或使用_PluckFieldType將它們寫在這里。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM