Typescript: how to achieve a recursion in this type?

佐手、 提交于 2019-12-01 21:18:06

I think in the other question and in comments I said that you can try to do this recursively but that the compiler, even if you can make it happy with the definition, will give up at some relatively shallow depth. Let's look at it here:

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;

EDIT: You asked for an explanation, so here it is:

First, the definition of Cons<H, T> uses 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" comes from list manipulation in Lisp and later in Haskell.) So Cons<"a", ["b","c"]> evaluates to ["a","b","c"].

As background, TypeScript generally tries to stop you from doing circular types. You can sneak around the back door by using some conditional types to defer some execution, like this:

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

This should either evaluate to BaseCase or RecursiveThing<...T...>, but the conditional type inside the index access gets deferred, and so the compiler doesn't realize that it will end up being a circular reference. Unfortunately this has the very bad side effect of causing the compiler to bottom out on infinite types and often getting bogged down when you start using these things in practice. For example, you can define TupleOfIncrementedLength<> like this:

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

and it works, even for N up to 40 or 50 or so. But if you stick this in a library and start using it you might find that your compile time becomes very high and even causes compiler crashes. It's not consistent, so I can't easily generate an example here, but it's bitten me enough for me to avoid it. It's possible this problem will eventually be fixed. But for now, I'd follow the advice of @ahejlsberg (lead architect for 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!

Enter @strax, who discovered that since interface declarations are not evaluated eagerly the way that type declarations are (since types are just aliases, the compiler tries to evaluate them away), if you can extend an interface in the right way, in conjunction with the conditional type trick above, the compiler should not get bogged down. My experiments confirm this, but I'm still not convinced... there could be a counterexample that only shows up when you try to compose these things.

Anyway, the above TupleOfIncrementedLength type with Reduction and Reduce works much the same way as the "naïve" version from before, except that it is an interface. I really can't pick it apart without writing ten pages and I don't feel like doing it, sorry. The actual output is a Reduction whose 1 property has the tuple we care about.

After that, Increment is defined in terms of TupleOfIncrementedLength by getting the 1 property and extracting its length (I don't use plain indexed access because the compiler can't deduce that TupleOfIncrementedLength<N>[1] is an array type. Luckily conditional type inference saves us).


I don't know that it's too useful for me to walk through exactly how this works. Suffice it to say that it keeps increasing the length of a tuple until its length is one greater than the N parameter, and then returns the length of that tuple. And it does work, for small N:

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

But, on my compiler at least (version 3.3.0-dev.20190117), this happens:

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

Also, you get some weirdness with unions and number:

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

You can probably fix both of these behaviors with conditional types, but this feels like you're bailing out a sinking ship.

If the above super-complicated and fragile method gives out entirely at 23, you might as well use a hardcoded list of outputs as I showed in the answer to the other question. For anyone playing along at home who wants to see answers in one place, it is:

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]

That is comparatively simpler, and works for higher numbers:

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

is generally more graceful when it fails, and fails at a known place:

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

and naturally does better things in the "weird" cases above

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

So, all in all, recursive types are more trouble than they're worth here, in my opinion.

Okay, hope that helps again. Good luck!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!