[英]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];
這里userOptionKeys
是UserOptions
對象中所需鍵的顯式有序列表......與我們手動創建的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_0
和args_1
這樣的標簽。 如果您希望它們是param1
和param2
,您只需要硬着頭皮寫出兩次參數名稱; 一次作為字符串文字,再次作為元組標簽,因為沒有辦法將一個轉換為另一個:
const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [param1: string, param2: number, thirdOne: boolean];
這值得么? 也許……這取決於你。
您可以這樣做,但是 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.