Ray-D-Song's Blog

TypeScript interface 隐式索引签名丢失问题

2024-09-20 3min

最近遇到一个奇怪的问题,interface 定义了一个符合要求的类型,却无法作为泛型参数。

ts
1type Foo = Record<string, unknown> 2 3type Test<K extends Foo> = K 4 5interface Bar { 6 'a': string 7} 8 9/** 10 * error: 11 * Type 'Bar' does not satisfy the constraint 'Foo'. 12 * Index signature for type 'string' is missing in type 'Bar'. 13 */ 14type Res = Test<Bar>

TS 提示 Bar 类型不满足 Foo 的定义,这本质是一个隐式索引签名丢失的问题。

什么是隐式索引签名

隐式索引签名(implicit index signatures)是 TypeScript 的一个特性,允许你定义一个对象类型,可以使用任意字符串作为键。
这意味着你可以创建一个对象,在这个对象中,键的名称是动态的,而值的类型是统一的。

例如

ts
1type Bar = { 2 [key: string]: number 3} 4const bar: Bar = { 5 'a': 1, 6 'b': 2 7}

解决方案

隐式索引丢失的问题仅限 interface,type 没有这个问题。
也就是说开头的例子,将 Bar 的定义修改为 type 即可:

ts
1type Foo = Record<string, unknown> 2 3type Test<K extends Foo> = K 4 5type Bar = { 6 a: string 7} 8 9type Res = Test<Bar>

如果你的类型不便修改,例如它是定义在 node_modules 中的库类型,你也可以使用Pick将 interface 转换为 type。

ts
1type Foo = Record<string, unknown> 2 3type Test<K extends Foo> = K 4 5interface Bar { 6 a: string 7} 8 9type Res = Test<Pick<Bar, keyof Bar>>

interface 这种行为其实符合 TypeScript team 的预期,相关讨论在 2017 年就已经有了。

https://github.com/microsoft/TypeScript/issues/15300

官方解释是: 「interface 可以通过同名接口合并进行增强,而 type 不可以,所以为 type 进行隐式索引推断会更安全」