網站搜尋

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>。根據你的使用情境,你可能會偏好這個解決方案而非另一個。再次祝你好運。