如何在打字稿中提取高阶函数的最终返回类型?

时间:2019-11-13 13:41:50

标签: typescript

我正试图创建一个FinalReturnType,以提取Typescript中高阶函数的最终返回类型:

type FinalReturnType = FinalReturnType<(a: string) => (b: string) => (c: string) => (d: string) => number>;
// expect FinalReturnType = number

我遇到了问题,因为它循环引用了自己。

我要写这个:

type FinalReturnType<F> =
  F extends (...args: any[]) => infer R
    ? FinalReturnType<R>
    : R;

错误:

Type alias 'FinalReturnType' circularly references itself.

这是我目前正在使用的-有点丑陋,限于四层深度:

type FinalReturnType<A> = A extends (...args: any[]) => infer R1
  ? R1 extends (...args: any[]) => infer R2
    ? R2 extends (...args: any[]) => infer R3
      ? R3 extends (...args: any[]) => infer R4
        ? R4
        : R3
      : R2
    : R1
  : never;

我觉得这应该可行,但是也许不是吗?

1 个答案:

答案 0 :(得分:1)

好的,将评论扩展为答案:

像您想用FinalReturnType那样创建的递归条件类型不受官方支持。请参见microsoft/TypeScript#26980,以寻求解除此约束并允许使用此类循环类型定义的建议。支持它们的问题(至少现在是这样)是,编译器通常会急切地评估类型,而不是推迟它们(有一些例外);当类型的定义依赖于自身时,如果允许此类编译器,则编译器将陷入循环。实际上,发生的情况是它要么注意到会有一个循环并立即警告您“类型别名循环引用自身”错误,要么出现其他类似的错误,例如“在其自己的类型注释中直接或间接引用X”, 实际上,它进入了循环,并在达到某些内部递归限制后,出现“类型实例化过深,甚至可能是无限的”形式的错误。

有好或坏的愚弄编译器的方法。它们通常使用旨在允许某些有限的类型评估延迟的功能。这样的功能之一就是您可以构建递归嵌套的对象类型,例如type LinkedList = {value: any, next?: LinkedList}。当您将内容推入对象属性时,编译器会推迟类型评估。这明确旨在允许递归,这是将您的脚踩到门中的一种步骤。结合使用键来look up个对象属性类型的功能,您将拥有一个危险的工具。

另一个此类功能是条件类型依赖于未指定的通用类型参数,例如type Foo<T> = T extends any ? true : false。除非您指定Foo<T>,否则该类型true的值将不会等于T。现在,如果您可以直接将此类条件类型设置为自引用,则可以完成,但是编译器会尝试注意并发出警告。但是,如果将此与上面的对象嵌套和查找技巧结合在一起,则会得到:

type FinalReturnType<T> = {
  0: T,
  1: FinalReturnType<ReturnType<Extract<T, (...a: any) => any>>>
}[T extends (...a: any) => any ? 1 : 0]

将递归位放入1属性中,通过查找将其拉出,并通过使其依赖于未解析的条件类型(没有直接递归)来推迟查找。它也有效:

type Str = FinalReturnType<() => () => () => () => () => string> // string

但是不要这样做!

它是not supported。是against the rules。不能保证在所有情况下都能正常工作。

例如,考虑一下:

interface Add { (val: number): Add, val: number };
function a(this: { val: number }, x: number): Add {
  return Object.assign(a.bind({ val: x + this.val }), { val: x + this.val });
}
const add: Add = a.bind({ val: 0 })(0);
console.log(add(1)(2)(3).val); // 6
console.log(add(4)(5)(6).val); // 15

Add的最终返回类型是什么?让我们看看:

type Whoopsie = FinalReturnType<Add>; // any

好吧,也许很好...呃,回头看看FinalReturnType的定义:

type FinalReturnType<T> = {
  0: T,
  1: FinalReturnType<ReturnType<Extract<T, (...a: any) => any>>> // error!
//~ <--- '1' is referenced directly or indirectly in its own type annotation
}[T extends (...a: any) => any ? 1 : 0]; 

现在有一个错误。


此问题肯定是可修补的。我之前做过的一件事是编写自己的递归限制器,如下所示:

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
type FinalReturnType<T, N extends number = 15> =
  0 extends N ? T : {
    0: T, 1: FinalReturnType<ReturnType<Extract<T, (...a: any) => any>>, Prev[N]>
  }[T extends (...a: any) => any ? 1 : 0]

type Str = FinalReturnType<() => () => () => () => () => string> // string
type OkayNow = FinalReturnType<Add>; // Add

FinalReturnType<T, N>的类型为T,递归限制为N,如果递归太远,则仅返回T来纾困。只要N保持小于模糊的内部编译器递归限制,它应该可能会起作用。仍然没有官方支持。


其他人还有其他说服编译器支持任意深度递归类型分析的方法。有一个ts-toolbelt库可能很快就会make it into TypeScript's test cases;一旦发生这种情况,您也许可以使用该库中的某些内容,因为我们至少希望它在更新语言时不会中断。但是目前它还不存在,所以我不建议您这样做。


通常,在这些情况下,我要做的是构建可以达到某个合理最大深度的东西,就像您已经完成了深度4一样。这种类型可能很难看,但是很简单,并且可以完成您已要求它这样做。您也许可以用不太难看的方式重写它,但是基本问题仍然存在:当前不直接支持循环条件类型。


好的,希望能有所帮助;祝你好运!

Link to code