TypeScript 中的類型化泛型鍵值介面
我有以下範例物件:
let foo: Foo = {
'key1': { default: 'foo', fn: (val:string) => val },
'key2': { default: 42, fn: (val:number) => val },
// this should throw an error, because type of default and fn don't match
'key3': { default: true, fn: (val:string) => val }
}
介面應該看起來像這樣:
interface Foo {
[key: string]: { default: T, fn: (val:T) => any }
}
當然這行不通,因為沒有定義 T。
所以我想到了要做這個:
interface FooValue<T> {
default: T;
fn: (val:T) => any;
}
interface Foo {
[key: string]: FooValue<?>
}
但我也卡在那裡了。因為我無法定義 FooValue 的泛型類型。
如果我使用 FooValue<any>,那當然所有東西都會被型別化為 any。雖然那樣做是行不通的。
我想確保 default 的類型和 fn 的參數類型始終相同。
有解決辦法嗎?還是說這無法做到?
答案#1
如何將 Foo<T> 定義為映射類型,像這樣:
interface FooValue<T> {
default: T;
fn: (val: T) => any;
}
type Foo<T> = {
[K in keyof T]: FooValue<T[K]>
}
在這種情況下,如果 T 是某個普通物件類型,例如 {a: string, b: number, c: boolean},那麼 Foo<T> 就是它的 Foo-化版本:{a: FooValue<string>, b: FooValue<number>, c: FooValue<boolean>}. 現在你可以建立一個輔助函數,該函數僅在傳入的物件字面量可以被推斷為某個類型 T 的 Foo<T> 時才接受它:
function asFoo<T>(foo: Foo<T>): Foo<T> {
return foo;
}
這個函數之所以可行,是因為 TypeScript 編譯器可以從映射類型進行推斷,允許它從 Foo<T> 推斷出 T。以下是其運作方式:
let foo = asFoo({
key1: { default: 'foo', fn: (val: string) => val },
key2: { default: 42, fn: (val: number) => val }
});
// inferred as { key1: FooValue<string>; key2: FooValue<number>;}
而這裡它失敗了:
let badFoo = asFoo(
key1: { default: 'foo', fn: (val: string) => val },
key2: { default: 42, fn: (val: number) => val },
key3: { default: true, fn: (val: string) => val }
});
// error! Types of property 'key3' are incompatible.
// Type 'boolean' is not assignable to type 'string'.
希望這對你有幫助。祝你好運!
更新:上述程式碼假設你可以接受 foo.key1.fn('abc') 被推斷為 any 型別,因為 FooValue<string>['fn'] 被定義為返回 any 的函數。它有點忽略了原始物件文字的輸出型別。如果你希望 foo 記住 其屬性 fn 方法的返回型別,你可以使用這個稍微不同的輔助函數:
function asFoo<T, F>(foo: F & Foo<T>): F {
return foo;
}
let foo = asFoo({
key1: { default: 'foo', fn: (val: string) => val },
key2: { default: 42, fn: (val: number) => val },
// next line would cause error
// key3: { default: true, fn: (val: string)=>val}
})
const key1fnOut = foo.key1.fn('s') // known to be string
const key2fnOut = foo.key2.fn(123) // known to be number
而這是可行的。在這種情況下,asFoo() 只是驗證輸入是否為某個 T 的 Foo<T>,但它不會將輸出類型強制轉換為 Foo<T>。根據你的使用情境,你可能會偏好這個解決方案而非另一個。再次祝你好運。
