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

Nur*_*yev 4 typescript recursive-type typescript-typings

我原来的问题是这样的

我做了以下类型,但由于循环引用错误它不起作用,我不知道如何解决它:

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>>>
Run Code Online (Sandbox Code Playgroud)

最后它应该像这样工作:

type five = 5
type six = Increment<five> // 6
Run Code Online (Sandbox Code Playgroud)

PSTupleUnshift来自这里

jca*_*alz 6

TS4.1+ 更新

欢迎回来!TypeScript 4.1 引入了递归条件类型,它与诸如可变元组类型之类的东西一起使得通过递归执行“向非负整数加一”成为可能。它实际上表现得很好,但我仍然不推荐它用于生产环境,原因我将很快介绍。


首先,我会恬不知耻地复制借用的技术此评论通过@lazytype在Microsoft /打字稿#26223这使得任何非负整数长度的元组没有砸入递归限制在约深度23.打破做到这一点将整数转换为 2 的不同幂的总和(即,使用其二进制表示)并连接这些长度的元组。可变元组类型可以很容易地将元组 ( [...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]
Run Code Online (Sandbox Code Playgroud)

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

type Increment<N extends number> = [0, ...TupleOf<0, N>]['length'];
Run Code Online (Sandbox Code Playgroud)

它创建了一个长度N为 0的元组(不管你在那里使用什么),在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
Run Code Online (Sandbox Code Playgroud)

好的!这看起来像我们想要的,我想。现在对于不太好的部分以及我不推荐它的原因:

// Don't do this
type Kablooey = Increment<3.14> // Loading... 
Run Code Online (Sandbox Code Playgroud)

那会导致编译器心脏病发作;递归元组构建器永远不会达到它的目标,因为无论它生成多大的元组, index 处都没有元素3.14


可以修复吗?当然。TupleOf如果数字的字符串表示中有小数点,我们可以在类型中添加一个额外的保护(对模板文字类型大喊大叫!):

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]
Run Code Online (Sandbox Code Playgroud)

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

type AlsoNumber = Increment<3.14> // number
Run Code Online (Sandbox Code Playgroud)

所以你去了;它运行良好。

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

相反,我仍然建议进行简单的查找,直到并且除非 TypeScript 按照microsoft/TypeScript#15645和/或microsoft/TypeScript#26382 中的要求对数字文字实现算术。

Playground 链接到代码



早期 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;
Run Code Online (Sandbox Code Playgroud)

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

首先,定义在休息/传播位置条件类型Cons<H, T>使用元组来获取头部类型和元组尾部类型,并返回一个新的元组,其中头部已被添加到尾部。(名称“cons”来自 Lisp 和后来的 Haskell 中的列表操作。)所以计算结果为.HTCons<"a", ["b","c"]>["a","b","c"]

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

type RecursiveThing<T> = 
  { 0: BaseCase, 1: RecursiveThing<...T...> }[Test<...T...> extends BaseCaseTest ? 0 : 1]
Run Code Online (Sandbox Code Playgroud)

这应该计算为BaseCaseor RecursiveThing<...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]
Run Code Online (Sandbox Code Playgroud)

它可以工作,甚至可以N达到 40 或 50 左右。但是如果你把它放在一个库中并开始使用它,你可能会发现你的编译时间变得非常长,甚至导致编译器崩溃。它不一致,所以我无法在这里轻松生成示例,但它足以让我避免它。这个问题最终可能会得到解决。但现在,我会按照建议@ahejlsberg(为打字稿首席架构师):

这很聪明,但它肯定会使事情远远超出其预期用途。虽然它可能适用于小例子,但它的扩展性会很差。解析那些深度递归类型会消耗大量时间和资源,并且将来可能会与我们在检查器中使用的递归调控器发生冲突。

不要这样做!

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

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

在此之后,Increment在来定义的TupleOfIncrementedLength通过获取1财产并提取其length(我不使用纯索引访问,因为编译器不能推断出TupleOfIncrementedLength<N>[1]是一个数组类型。幸运的是有条件的类型推断为我们节省了)。


我不知道详细介绍它的工作原理对我来说太有用了。 可以说它不断增加元组的长度,直到它的长度比N参数大 1,然后返回该元组的长度。它确实有效,对于小N

type Seven = Increment<6>; // 7
type Sixteen = Increment<15>; // 16
Run Code Online (Sandbox Code Playgroud)

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

type TwentyThree = Increment<22>; // 23
type TwentyWhaaaa = Increment<23>; // {} 
type Whaaaaaaaaaa = Increment<100>; // {} 
Run Code Online (Sandbox Code Playgroud)

此外,您对工会和number以下内容感到有些奇怪:

type WhatDoYouThink = Increment<5 | 10>; // 6 
type AndThis = Increment<number>; // 1 
Run Code Online (Sandbox Code Playgroud)

您可能可以使用条件类型来修复这两种行为,但这感觉就像您正在救助一艘正在沉没的船。

如果上述超级复杂和脆弱的方法在 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]
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

失败时通常更优雅,并在已知位置失败:

type NotFiftyFiveButNumber = Increment<54>; // number
type NotOneHundredOneButNumber = Increment<100>; // number
Run Code Online (Sandbox Code Playgroud)

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

type WhatDoYouThink = Increment<5 | 10>; // 6 | 11 
type AndThis = Increment<number>; // number 
Run Code Online (Sandbox Code Playgroud)

所以,总而言之,在我看来,递归类型比它们在这里的价值更麻烦。

好的,希望再次帮助。祝你好运!