为什么TypeScript编译器不允许这种看似有效的代码?

axzmvihb  于 2023-03-04  发布在  TypeScript
关注(0)|答案(1)|浏览(395)

我无法理解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函数(您可以在操场上查看它)。
这失败有什么具体原因吗?有没有可能用其他方式来表达这一点?

hmae6n7t

hmae6n7t1#

你的版本不起作用的原因是因为编译器并没有真正尝试计算依赖于generic类型参数的conditional types,在doWork()内部,编译器看到你调用的是一个FnMap[T]类型的函数,参数是FInput<FnMap[T]>类型的,但是它不能将其结果连接到FOutput<FnMap[T]>FInputFOutput是条件类型,而T是泛型,所以编译器不会做太多的分析,它最后做的是把T扩展到它的约束,然后看到它在FnMap中调用了 * some * 函数,带有 * some * 参数,但是它没有注意到它们匹配,并且它抱怨输入可能不合适,并且它也不能确定输出是否正确。
这里推荐的方法在microsoft/TypeScript#47109中有描述,它涉及到重构,以便根据mapped types上的泛型indexed accesses显式地表示事物。目标是函数对于某些XXXYYY是类似(arg: XXX) => YYY的某种类型,并且输入将是XXX类型。因此编译器可以直接得出结论,它生成了YYY。对于人类来说,重构可能看起来好像没有发生任何重要的事情,因为它与您的版本等效,但编译器可以更好地遵循逻辑。
下面是我对示例代码的处理方法:首先,显式定义Map对象,并从中派生一个类型,这两个类型都对应于原始的非重构类型:

const _fnMap = {
  numToInt: (x: number) => Number(x.toFixed(0)),
  numToStr: (x: number) => x.toString(10),
  strToNum: (x: string) => parseInt(x),
  strToStr: (x: string) => x.toUpperCase(),
}
type _FnMap = typeof _fnMap;

现在我们可以产生三种Map类型;一个用于输入,一个用于输出,一个用于从输入到输出的功能:

type FnMapArgs = { [K in keyof _FnMap]: Parameters<_FnMap[K]>[0] };
type FnMapRets = { [K in keyof _FnMap]: ReturnType<_FnMap[K]> }
type FnMap = { [K in keyof _FnMap]: (input: FnMapArgs[K]) => FnMapRets[K] };

关键是这些类型不是泛型的,而是特定的,编译器可以很好地计算非泛型的条件类型,所以_FnMapFnMap是等价的类型,编译器理解这一点......因此我们可以将_FnMap类型的值赋给FnMap类型的变量,而不会出错:

const fnMap: FnMap = _fnMap;

现在我们可以将doWork()作为泛型索引访问显式地写入Map类型:

function doWork<K extends keyof _FnMap>(type: K, input: FnMapArgs[K]): FnMapRets[K] {
  return fnMap[type](input); // okay
}

在函数内部,对于泛型KfnMap[type]的类型为FnMap[K],编译器可以根据FnMap的定义将其计算为(input: FnMapArgs[K]) => FnMapRets[K]。由于input的类型为FnMapArgs[K],因此编译器很高兴地允许调用,并生成FnMapRets[K]类型的结果。这是函数的期望输出类型。
Playground代码链接

相关问题