Typescript: ¿cómo lograr una recursión en este tipo?
Mi problema original es este.
Hice el siguiente tipo, pero no funciona debido al error de referencias circulares y no sé cómo solucionarlo:
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>>>
Al final debería funcionar así:
type five = 5
type six = Increment<five> // 6
P.D. TupleUnshift es de aquí.
Respuesta #1
ACTUALIZACIÓN PARA TS4.1+
¡Bienvenido de nuevo! TypeScript 4.1 introdujo tipos condicionales recursivos, los cuales, junto con cosas como los tipos de tupla variádica, han hecho posible realizar un "sumar uno a un número entero no negativo" mediante recursión. En realidad, se comporta bastante bien, pero todavía no lo recomiendo para entornos de producción por razones que explicaré enseguida.
En primer lugar, voy a copiar sin vergüenza tomar prestada una técnica de este comentario de @lazytype en microsoft/TypeScript#26223 que crea una tupla de cualquier longitud de número entero no negativo sin chocar con el límite de recursión alrededor de la profundidad 23. Lo hace descomponiendo el número entero en una suma de potencias de dos distintas (es decir, usando su representación binaria) y concatenando tuplas de estas longitudes. Los tipos de tupla variádica facilitan lo suficiente duplicar la longitud de una tupla ([...T, ...T]), por lo que esta técnica admite tuplas de longitudes que llegan a miles y decenas de miles:
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]
Entonces el tipo Increment que quieres es este:
type Increment<N extends number> = [0, ...TupleOf<0, N>]['length'];
lo que crea una tupla de longitud N llena de ceros (no importa qué uses allí), le antepone un único 0 y obtiene su longitud.
Veámoslo en acción:
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
¡Genial! Eso parece lo que queremos, creo. Ahora viene la parte no tan agradable y la razón por la que no lo recomiendo:
// Don't do this
type Kablooey = Increment<3.14> // Loading... 💥🔥💻🔥💥
Eso hará que el compilador tenga un ataque al corazón; el constructor de tuplas recursivo nunca alcanza su objetivo porque sin importar cuán grande sea la tupla que haga, nunca hay un elemento en el índice 3.14.
¿Se puede arreglar eso? Claro. Podemos añadir una protección extra en el tipo TupleOf para salir si hay un punto decimal en la representación en cadena del número (¡un saludo a los tipos literales de plantilla!):
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]
Ahora 3.14 resulta en number, que no es 4.14, pero al menos es razonable y no hace que el compilador explote:
type AlsoNumber = Increment<3.14> // number
Así que ahí lo tienes; funciona y rinde bien.
Todavía no puedo obligarme a decirle a nadie "por favor, use esto en su entorno de producción", sin embargo. Simplemente es demasiado potencialmente frágil. ¿Qué tan seguro estás, mirando la complicada manipulación de tipos de arriba, de que no haya algún caso límite simple que haga que tu compilador se bloquee? ¿Quieres ser responsable de parchear esas cosas solo para que el compilador sume uno a un número?
En su lugar, todavía recomendaría una búsqueda simple hasta que TypeScript implemente operaciones aritméticas en literales numéricos como se solicita en microsoft/TypeScript#15645 y/o microsoft/TypeScript#26382.
Enlace de Playground al código
RESPUESTA ANTERIOR PARA VERSIONES ANTERIORES DE TS:
Creo que en la otra pregunta y en los comentarios dije que puedes intentar hacer esto recursivamente, pero que el compilador, incluso si puedes hacer que esté contento con la definición, se rendirá a una profundidad relativamente superficial. Veámoslo aquí:
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;
EDITAR: Pediste una explicación, así que aquí está:
Primero, la definición de Cons<H, T> utiliza tuplas en posiciones de resto/spread y tipos condicionales para tomar un tipo de cabeza H y un tipo de tupla cola T y devolver una nueva tupla en la que la cabeza se ha antepuesto a la cola. (El nombre "cons" proviene de la manipulación de listas en Lisp y más tarde en Haskell.) Así, Cons<"a", ["b", "c"]> se evalúa como ["a", "b", "c"].
Como contexto, TypeScript generalmente intenta impedir que hagas tipos circulares. Puedes eludir esto por la puerta trasera usando algunos tipos condicionales para diferir cierta ejecución, así:
type RecursiveThing<T> =
{ 0: BaseCase, 1: RecursiveThing<...T...> }[Test<...T...> extends BaseCaseTest ? 0 : 1]
Esto debería evaluarse a BaseCase o RecursiveThing<...T...>, pero el tipo condicional dentro del acceso de índice se pospone, y por lo tanto el compilador no se da cuenta de que terminará siendo una referencia circular. Desafortunadamente, esto tiene el muy mal efecto secundario de hacer que el compilador se bloquee con tipos infinitos y, a menudo, se atasque cuando comienzas a usar estas cosas en la práctica. Por ejemplo, puedes definir TupleOfIncrementedLength<> de esta manera:
type TupleOfIncrementedLength<N extends number, Tuple extends any[]=[]> = {
0: Cons<any, Tuple>,
1: TupleOfIncrementedLength<N, Cons<any, Tuple>>
}[Tuple['length'] extends N ? 0 : 1]
y funciona, incluso para N de hasta 40 o 50 más o menos. Pero si lo pones en una biblioteca y comienzas a usarlo, podrías encontrar que tu tiempo de compilación se vuelve muy alto e incluso causa fallos del compilador. No es consistente, así que no puedo generar fácilmente un ejemplo aquí, pero me ha pasado lo suficiente como para evitarlo. Es posible que este problema eventualmente se solucione. Pero por ahora, seguiría el consejo de @ahejlsberg (arquitecto principal de TypeScript):
Es ingenioso, pero definitivamente lleva las cosas mucho más allá de su uso previsto. Aunque puede funcionar para ejemplos pequeños, escalará horriblemente. Resolver esos tipos profundamente recursivos consume mucho tiempo y recursos y podría en el futuro chocar con los reguladores de recursión que tenemos en el verificador.
¡No lo hagas!
Entra @strax, quien descubrió que dado que las declaraciones de interface no se evalúan de manera ansiosa como lo hacen las declaraciones de type (ya que los type son solo alias, el compilador intenta evaluarlos hasta eliminarlos), si puedes extender una interface de la manera correcta, en conjunto con el truco de tipo condicional mencionado anteriormente, el compilador no debería atascarse. Mis experimentos confirman esto, pero todavía no estoy convencido... podría haber un contraejemplo que solo aparezca cuando intentas componer estas cosas.
De todos modos, el tipo TupleOfIncrementedLength mencionado arriba con Reduction y Reduce funciona de manera muy similar a la versión "ingenua" de antes, excepto que es una interface. Realmente no puedo desmenuzarlo sin escribir diez páginas y no tengo ganas de hacerlo, lo siento. La salida real es un Reduction cuya propiedad 1 tiene la tupla que nos interesa.
Después de eso, Increment se define en términos de TupleOfIncrementedLength obteniendo la propiedad 1 y extrayendo su length (no uso acceso indexado simple porque el compilador no puede deducir que TupleOfIncrementedLength<N>[1] es un tipo de arreglo. Afortunadamente, la inferencia de tipos condicional nos salva).
No sé si me resulta muy útil explicar exactamente cómo funciona esto. Basta decir que sigue aumentando la longitud de una tupla hasta que su longitud sea una unidad mayor que el parámetro N, y luego devuelve la longitud de esa tupla. Y funciona, para valores pequeños de N:
type Seven = Increment<6>; // 7
type Sixteen = Increment<15>; // 16
Pero, en mi compilador al menos (versión 3.3.0-dev.20190117), esto sucede:
type TwentyThree = Increment<22>; // 23
type TwentyWhaaaa = Increment<23>; // {} 😞
type Whaaaaaaaaaa = Increment<100>; // {} 🙁
Además, obtienes algunas rarezas con las uniones y number:
type WhatDoYouThink = Increment<5 | 10>; // 6 😕
type AndThis = Increment<number>; // 1 😕
Probablemente puedas arreglar ambos comportamientos con tipos condicionales, pero esto se siente como si estuvieras rescatando un barco que se hunde.
Si el método súper complicado y frágil mencionado anteriormente falla por completo a los 23, bien podrías usar una lista de salidas codificada como mostré en la respuesta a la otra pregunta. Para cualquiera que esté siguiendo desde casa y quiera ver las respuestas en un solo lugar, es:
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]
Eso es relativamente más simple y funciona para números más grandes:
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
generalmente es más elegante cuando falla, y falla en un lugar conocido:
type NotFiftyFiveButNumber = Increment<54>; // number
type NotOneHundredOneButNumber = Increment<100>; // number
y naturalmente hace mejores cosas en los casos "extraños" mencionados arriba
type WhatDoYouThink = Increment<5 | 10>; // 6 | 11 👍
type AndThis = Increment<number>; // number 👍
Así que, en general, los tipos recursivos son más problemáticos de lo que valen aquí, en mi opinión.
Está bien, espero que eso ayude de nuevo. ¡Buena suerte!
