我无法理解TypeScript类型检查器为什么拒绝看似有效的代码。
export type Fn<I, O> = (value: I) => O
type FInput<F extends Fn<any, any>> = F extends Fn<infer I, any> ? I : never
type FOutput<F extends Fn<any, any>> = F extends Fn<any, infer O> ? O : never
type FnMap = {
numToInt: Fn<number, number>
numToStr: Fn<number, string>
strToNum: Fn<string, number>
strToStr: Fn<string, string>
}
const fnMap: FnMap = {
numToInt: (x) => Number(x.toFixed(0)),
numToStr: (x) => x.toString(10),
strToNum: (x) => parseInt(x),
strToStr: (x) => x.toUpperCase(),
} as const
function doWork<T extends keyof FnMap>(key: T, input: FInput<FnMap[T]>): FOutput<FnMap[T]> {
const fn = fnMap[key]
return fn(input)
// Type 'string | number' is not assignable to type 'FOutput<FnMap[T]>'.
// Type 'string' is not assignable to type 'FOutput<FnMap[T]>'.
// Argument of type 'string | number' is not assignable to parameter of type 'never'.
// Type 'string' is not assignable to type 'never'.
}
给定上面的示例,TypeScript正确地自动完成了doWork
函数(您可以在操场上查看它)。
这失败有什么具体原因吗?有没有可能用其他方式来表达这一点?
1条答案
按热度按时间hmae6n7t1#
你的版本不起作用的原因是因为编译器并没有真正尝试计算依赖于generic类型参数的conditional types,在
doWork()
内部,编译器看到你调用的是一个FnMap[T]
类型的函数,参数是FInput<FnMap[T]>
类型的,但是它不能将其结果连接到FOutput<FnMap[T]>
。FInput
和FOutput
是条件类型,而T
是泛型,所以编译器不会做太多的分析,它最后做的是把T
扩展到它的约束,然后看到它在FnMap
中调用了 * some * 函数,带有 * some * 参数,但是它没有注意到它们匹配,并且它抱怨输入可能不合适,并且它也不能确定输出是否正确。这里推荐的方法在microsoft/TypeScript#47109中有描述,它涉及到重构,以便根据mapped types上的泛型indexed accesses显式地表示事物。目标是函数对于某些
XXX
和YYY
是类似(arg: XXX) => YYY
的某种类型,并且输入将是XXX
类型。因此编译器可以直接得出结论,它生成了YYY
。对于人类来说,重构可能看起来好像没有发生任何重要的事情,因为它与您的版本等效,但编译器可以更好地遵循逻辑。下面是我对示例代码的处理方法:首先,显式定义Map对象,并从中派生一个类型,这两个类型都对应于原始的非重构类型:
现在我们可以产生三种Map类型;一个用于输入,一个用于输出,一个用于从输入到输出的功能:
关键是这些类型不是泛型的,而是特定的,编译器可以很好地计算非泛型的条件类型,所以
_FnMap
和FnMap
是等价的类型,编译器理解这一点......因此我们可以将_FnMap
类型的值赋给FnMap
类型的变量,而不会出错:现在我们可以将
doWork()
作为泛型索引访问显式地写入Map类型:在函数内部,对于泛型
K
,fnMap[type]
的类型为FnMap[K]
,编译器可以根据FnMap
的定义将其计算为(input: FnMapArgs[K]) => FnMapRets[K]
。由于input
的类型为FnMapArgs[K]
,因此编译器很高兴地允许调用,并生成FnMapRets[K]
类型的结果。这是函数的期望输出类型。Playground代码链接