TypeScript映射类型疑问:如何将接口值类型用作函数参数类型?
TypeScript映射类型函数参数兼容性问题解决
问题代码与报错
先看这段代码,目标是给Test的每个属性生成对应的校验函数:
type validTypes = string | number type item = Record<string, validTypes> interface Test extends item { someint: number; something: string; } let test: { [P in keyof Test]: (arg: Test[P]) => boolean } = { someint: (arg) => arg > 0, something: (arg) => arg == "hi" }
执行时报错:
Type '{ someint: (arg: number) => boolean; something: (arg: string) => boolean; }' is not assignable to type '{ [x: string]: (arg: validTypes) => boolean; someint: (arg: number) => boolean; something: (arg: string) => boolean; }'. Property 'someint' is incompatible with index signature. Type '(arg: number) => boolean' is not assignable to type '(arg: validTypes) => boolean'. Types of parameters 'arg' and 'arg' are incompatible. Type 'validTypes' is not assignable to type 'number'. Type 'string' is not assignable to type 'number'.
奇怪的是,要是映射类型改成[P in keyof Test]: Test[P]就完全正常,只有把Test[P]用作函数参数时才会出问题。
问题根源
核心原因是函数参数的逆变特性,加上Test继承了Record<string, validTypes>带来的索引签名:
- 因为
Test extends item,所以keyof Test不仅包含"someint"、"something"这两个字面量键,还包含了string类型的索引签名(对应Record<string, validTypes>里的任意字符串键)。 - 映射类型会为这个
string索引生成[x: string]: (arg: validTypes) => boolean的约束,要求所有字符串键对应的函数都能接受validTypes(即string | number)类型的参数。 - 但
someint对应的函数是(arg: number) => boolean,它只能接受number类型,无法处理string类型参数——这违反了函数参数的逆变规则:只有参数类型更宽泛的函数,才能兼容参数类型更窄的函数(比如(arg: validTypes) => boolean可以赋值给(arg: number) => boolean,反过来不行)。 - 而不带函数的映射类型没问题,是因为值类型是协变的:
number是validTypes的子类型,所以可以直接兼容。
解决方案
方案1:移除不必要的继承
如果你的Test接口不需要支持任意字符串键的validTypes属性,直接去掉extends item即可:
type validTypes = string | number interface Test { someint: number; something: string; } let test: { [P in keyof Test]: (arg: Test[P]) => boolean } = { someint: (arg) => arg > 0, something: (arg) => arg == "hi" }
此时keyof Test只有两个字面量键,映射类型不会生成索引签名,自然不存在兼容性问题。
方案2:保留继承但排除索引签名
如果必须保留Test extends item的继承关系,可以用Exclude<keyof Test, string>排除掉索引签名对应的string类型键,只针对具体的字面量键做映射:
type validTypes = string | number type item = Record<string, validTypes> interface Test extends item { someint: number; something: string; } let test: { [P in Exclude<keyof Test, string>]: (arg: Test[P]) => boolean } = { someint: (arg) => arg > 0, something: (arg) => arg == "hi" }
这样映射类型只会生成someint和something对应的函数类型约束,不会带索引签名,也就解决了兼容性冲突。
内容的提问来源于stack exchange,提问作者octodecillion
相关产品推荐
相关产品推荐

