打字稿:如何实现这种类型的递归?

Typescript: how to achieve a recursion in this type?

我原来的问题是

我做了以下类型,但由于循环引用错误而无法使用,我不知道如何解决:

type Increment<T extends number, Tuple extends any[] = [any]> = 
T extends 0 ? 1 :
T extends 1 ? 2 :
T extends TupleUnshift<any, Tuple> ? 
TupleUnshift<any, TupleUnshift<any, Tuple>>['length'] :
Increment<T, TupleUnshift<any, TupleUnshift<any, Tuple>>>

最后它应该像这样工作:

type five = 5
type six = Increment<five> // 6

P.S。 TupleUnshift 来自 .

TS4.1+更新

欢迎回来! TypeScript 4.1 引入 recursive conditional types, which, along with things like variadic tuple types 使得通过递归执行“对非负整数加一”成为可能。它实际上表现得很好,但我仍然不推荐它用于生产环境,原因我稍后会讲到。


首先,我会无耻地复制借鉴microsoft/TypeScript#26223中this comment by @lazytype的一个技巧,它可以生成任意非负整数的元组长度而不会在大约深度 23 处进入递归限制。它通过将整数分解为不同的两个幂的和(即,使用其二进制表示)并连接这些长度的元组来实现。可变元组类型可以很容易地将元组的长度加倍 ([...T, ...T]),因此该技术支持长度达到数千和数万的元组:

type BuildPowersOf2LengthArrays<N extends number, R extends never[][]> =
  R[0][N] extends never ? R : BuildPowersOf2LengthArrays<N, [[...R[0], ...R[0]], ...R]>;

type ConcatLargestUntilDone<N extends number, R extends never[][], B extends never[]> =
  B["length"] extends N ? B : [...R[0], ...B][N] extends never
  ? ConcatLargestUntilDone<N, R extends [R[0], ...infer U] ? U extends never[][] ? U : never : never, B>
  : ConcatLargestUntilDone<N, R extends [R[0], ...infer U] ? U extends never[][] ? U : never : never, [...R[0], ...B]>;

type Replace<R extends any[], T> = { [K in keyof R]: T }

type TupleOf<T, N extends number> = number extends N ? T[] : {
  [K in N]:
  BuildPowersOf2LengthArrays<K, [[never]]> extends infer U ? U extends never[][]
  ? Replace<ConcatLargestUntilDone<K, U, []>, T> : never : never;
}[N]

那么你要的Increment类型就是这样:

type Increment<N extends number> = [0, ...TupleOf<0, N>]['length'];

创建一个长度为 N 的元组,其中用零填充(不管你在那里使用什么),在它前面加上一个 0,并得到它的长度。

让我们看看实际效果:

type Seven = Increment<6>; // 7
type Sixteen = Increment<15>; // 16
type OneHundredOne = Increment<100>; // 101
type OneThousand = Increment<999>;  // 1000
type SixOrElevent = Increment<5 | 10>; // 6 | 11
type Numbah = Increment<number>; // number

不错!我认为这看起来像我们想要的。现在说说不好的部分以及我不推荐它的原因:

// Don't do this
type Kablooey = Increment<3.14> // Loading... 

那会导致编译器心脏病发作;递归的元组构建器永远不会达到它的目标,因为无论它创建的元组有多大,索引 3.14.

处永远不会有元素

可以修复吗?当然。如果数字的字符串表示形式中有小数点,我们可以在 TupleOf 类型中添加一个额外的守卫(大喊 template literal types!):

type TupleOf<T, N extends number> = number extends N ? T[] :
  `${N}` extends `${infer X}.${infer Y}` ? T[] : {
    [K in N]:
    BuildPowersOf2LengthArrays<K, [[never]]> extends infer U ? U extends never[][]
    ? Replace<ConcatLargestUntilDone<K, U, []>, T> : never : never;
  }[N]

现在 3.14 结果是 number,这不是 4.14,但至少是合理的,而不是编译器爆炸:

type AlsoNumber = Increment<3.14> // number

到此为止;它运行良好。

不过,我仍然无法告诉任何人“请在您的生产环境中使用它”。它太脆弱了。查看上面的拜占庭类型修改,您有多确定没有一些简单的边缘情况会使您的编译器崩溃?你想负责修补这些东西只是为了让编译器给数字加一吗?

相反,我仍然建议进行简单查找,直到并且除非 TypeScript 按照 microsoft/TypeScript#15645 and/or microsoft/TypeScript#26382.

中的要求对数字文字执行算术运算

Playground link to code



早期 TS 版本的先前答案:

我认为在另一个问题和评论中我说过你可以尝试递归地做这个但是编译器,即使你可以让它对定义满意,会在一些相对较浅的深度放弃。让我们在这里看看:

interface Reduction<Base, In> {
  0: Base
  1: In
}
type Reduce<T, R extends Reduction<any, any>, Base =[]> =
  R[[T] extends [Base] ? 0 : 1]

type Cons<H, T extends any[]> = ((h: H, ...t: T) => void) extends
  ((...c: infer C) => void) ? C : never;

interface TupleOfIncrementedLength<N extends number, Tuple extends any[]=[]>
  extends Reduction<Tuple, Reduce<Tuple['length'],
    TupleOfIncrementedLength<N, Cons<any, Tuple>>, N
  >> { }

type Increment<N extends number> = TupleOfIncrementedLength<N>[1] extends
  { length: infer M } ? M : never;

编辑:您要求解释,所以这里是:

首先,Cons<H, T> 的定义使用 tuples in rest/spread positions and conditional types to take a head type H and a tuple tail type T and return a new tuple in which the head has been prepended to the tail. (The name "cons" 来自 Lisp 中的列表操作和后来的 Haskell。)所以 Cons<"a", ["b","c"]> 计算为 ["a","b","c"] .

作为背景,TypeScript 通常会尝试阻止您使用循环类型。你可以偷偷绕过后门,使用一些 conditional types 来推迟一些执行,就像这样:

type RecursiveThing<T> = 
  { 0: BaseCase, 1: RecursiveThing<...T...> }[Test<...T...> extends BaseCaseTest ? 0 : 1]

这应该计算为 BaseCaseRecursiveThing<...T...>,但是索引访问中的条件类型被延迟,因此编译器没有意识到它最终会成为循环引用.不幸的是,这具有 非常糟糕的 副作用,导致编译器在无限类型上触底,并且当您在实践中开始使用这些东西时经常陷入困境。例如,您可以这样定义 TupleOfIncrementedLength<>

type TupleOfIncrementedLength<N extends number, Tuple extends any[]=[]> = {
  0: Cons<any, Tuple>,
  1: TupleOfIncrementedLength<N, Cons<any, Tuple>>
}[Tuple['length'] extends N ? 0 : 1]

它有效,即使 N 最多 40 或 50 左右。但是如果你把它放在一个库中并开始使用它,你可能会发现你的编译时间变得非常长,甚至会导致编译器崩溃。它不一致,所以我不能在这里轻易地生成一个例子,但它已经足够让我避免它了。这个问题最终可能会得到解决。但现在,我会跟随 advice of @ahejlsberg(TypeScript 的首席架构师):

It's clever, but it definitely pushes things well beyond their intended use. While it may work for small examples, it will scale horribly. Resolving those deeply recursive types consumes a lot of time and resources and might in the future run afoul of the recursion governors we have in the checker.

Don't do it!

输入 @strax, who discovered 因为 interface 声明不像 type 声明那样急切求值(因为 type 只是别名,编译器会尝试求值他们离开),如果你能以正确的方式扩展 interface ,结合上面的条件类型技巧,编译器应该不会陷入困境。我的实验证实了这一点,但我仍然不相信......可能会有一个反例,只有当你尝试编写这些东西时才会出现。

无论如何,上面的 TupleOfIncrementedLength 类型与 ReductionReduce 的工作方式与之前的“naïve”版本大致相同,除了它是一个 interface.不写十页我真的不能把它拆开,我也不想做,对不起。实际输出是 Reduction1 属性 有我们关心的元组。

之后,通过获取 1 属性 并提取其 length,根据 TupleOfIncrementedLength 定义 Increment(我不使用普通索引访问,因为编译器无法推断 TupleOfIncrementedLength<N>[1] 是数组类型。幸运的是条件类型推断拯救了我们。


我不知道它对我来说太有用了,无法详细了解它是如何工作的。我只想说它不断增加元组的长度,直到它的长度比 N 参数大一,然后 returns 该元组的长度。它确实有效,对于小 N:

type Seven = Increment<6>; // 7
type Sixteen = Increment<15>; // 16

但是,至少在我的编译器(版本 3.3.0-dev.20190117)上,会发生这种情况:

type TwentyThree = Increment<22>; // 23
type TwentyWhaaaa = Increment<23>; // {} 
type Whaaaaaaaaaa = Increment<100>; // {} 

此外,联合会有些奇怪 number:

type WhatDoYouThink = Increment<5 | 10>; // 6 
type AndThis = Increment<number>; // 1 

您可能可以使用 conditional types 来解决这两种行为,但这感觉就像您在救一艘正在下沉的船。

如果上述超级复杂和脆弱的方法在 23 时完全失效,您不妨使用我在 中展示的硬编码输出列表。对于任何想在一个地方看到答案的在家玩的人来说,它是:

type Increment<N extends number> = [
  1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,
  21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,
  38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54, // as far as you need
  ...number[] // bail out with number
][N]

相对简单,适用于更高的数字:

type Seven = Increment<6>; // 7
type Sixteen = Increment<15>; // 16
type TwentyThree = Increment<22>; // 23
type TwentyFour = Increment<23>; // 24
type FiftyFour = Increment<53>; // 54

一般失败时比较优雅,失败在已知的地方:

type NotFiftyFiveButNumber = Increment<54>; // number
type NotOneHundredOneButNumber = Increment<100>; // number

并且在上述“怪异”情况下自然会做得更好

type WhatDoYouThink = Increment<5 | 10>; // 6 | 11 
type AndThis = Increment<number>; // number 

所以,总而言之,在我看来,递归类型带来的麻烦超过了它们的价值。

好的,希望再次对您有所帮助。祝你好运!