为何TypeScript针对泛型索引类型抛出该错误?
MapToFunctions泛型不兼容 先看定义的泛型类型:
type MapToFunctions<T> = { [K in keyof T]?: (x: T[K]) => void; };
在无索引签名的类型下可以正常工作:
type T1 = { a: string }; const fnmap1: MapToFunctions<T1> = { a: (x: string) => { console.log(x); }, }
甚至可以省略x的类型注解,TypeScript会自动推断其为string类型。
但当泛型参数类型添加索引签名后,就会出现错误:
type T2 = { a: string; [key: string]: unknown; } const fnmap2: MapToFunctions<T2> = { a: (x: string) => { console.log(x); }, }
此时抛出的错误为:
Type '{ a: (x: string) => void; }' is not assignable to type 'MapToFunctions<T2>'. Property 'a' is incompatible with index signature. Type '(x: string) => void' is not assignable to type '(x: unknown) => void'. Types of parameters 'x' and 'x' are incompatible. Type 'unknown' is not assignable to type 'string'.ts(2322)
即便省略x的类型注解,TypeScript能推断出x是string,但错误依然存在。
原因分析
这是因为TypeScript中,当类型同时包含显式属性和索引签名时,显式属性的类型必须兼容索引签名的类型。
在T2中,a的类型是string,而索引签名[key: string]的类型是unknown,string兼容unknown(string是unknown的子类型),所以T2本身是合法的。
但当通过MapToFunctions<T2>映射后,生成的类型会同时包含:
- 显式属性
a?: (x: string) => void - 索引签名
[key: string]?: (x: unknown) => void
此时问题出现了:函数类型的兼容性是逆变的——也就是说,(x: string) => void并不兼容(x: unknown) => void。因为如果遵循索引签名的规则,允许传入unknown类型的参数给这个函数,但(x: string) => void无法接受unknown类型的参数(unknown不能直接赋值给string)。
TypeScript会要求显式属性的类型必须符合索引签名的约束,所以a对应的函数类型必须兼容索引签名对应的函数类型,而这里显然不满足,因此抛出错误。
简单来说,虽然T2中a的类型是string,但索引签名会让TypeScript认为T2的所有字符串键对应的类型都可以是unknown,映射后的函数类型自然也要遵循这个约束,导致显式属性的函数类型不符合要求。
内容的提问来源于stack exchange,提问作者aldel

