En las primeras versiones de TypeScript, los arrays y las tuplas tenían una separación conceptual estricta: los arrays T[] poseían una longitud arbitraria con un único tipo homogéneo de elementos, mientras que las tuplas [A, B, C] representaban estructuras heterogéneas estrictamente fijas. Cualquier intento de concatenar, recortar o invertir tuplas requería engorrosas sobrecargas manuales de funciones (a menudo entre 10 y 15 sobrecargas por cada paso de composición).
La auténtica revolución llegó con la introducción de los Variadic Tuple Types (Tipos de tupla variádicos) en TypeScript 4.0. El operador spread (...T) obtuvo soporte de primera clase dentro de las posiciones de tupla, permitiendo modelar secuencias abstractas de argumentos de dimensionalidad arbitraria: [First, ...Middle[], Last].
El segundo hito crítico fue la llegada de la Tail-Call Optimization on Types (TCO a nivel de tipos) en TypeScript 4.5. Hasta ese momento, el compilador imponía un límite estricto en la profundidad de la pila de instanciación de tipos recursivos (~50 pasos), abortando la compilación con el error fatal TS2589: "Type instantiation is excessively deep and possibly infinite".
Gracias al mecanismo TCO, los tipos condicionales recursivos diseñados mediante recursión por cola con un parámetro acumulador son optimizados por el compilador en un bucle iterativo plano. Esto incrementó la profundidad admisible de recursión hasta 1000 invocaciones, desbloqueando implementaciones completas de algoritmos clásicos de programación funcional (Reverse, Filter, Map, Curry) directamente en el sistema de tipos estático.
La diferencia fundamental entre la recursión estándar (basada en pila) y la recursión por cola optimizada en el compilador de TypeScript radica en dónde se acumula la estructura del tipo resultante:
+-------------------------------------------------------------------------+
| RECURSIÓN ESTÁNDAR (Límite: ~50 pasos) |
| Reverse<[1, 2, 3]> => [...Reverse<[2, 3]>, 1] |
+-------------------------------------------------------------------------+
| La pila del compilador debe conservar operaciones spread pendientes: |
| [... [... [...Reverse<[]>, 3], 2], 1] |
| => ¡Desbordamiento de pila de tsc cuando N > 50 (Error TS2589)! |
+-------------------------------------------------------------------------+
| |
| RECURSIÓN POR COLA (TCO) (¡Límite: 1000 pasos!) |
| ReverseTCO<T, Acc = []> |
+-------------------------------------------------------------------------+
| El compilador traslada de inmediato el resultado al acumulador: |
| 1. Paso 1: T = [2, 3], Acc = [1] |
| 2. Paso 2: T = [3], Acc = [2, 1] |
| 3. Paso 3: T = [], Acc = [3, 2, 1] |
| => Salida terminal: ¡devuelve Acc directamente sin retener marcos! |
+-------------------------------------------------------------------------+En la recursión por cola, el tipo condicional devuelve una llamada directa y limpia a sí mismo, sin envolverla en tipos exteriores (tuplas, uniones o intersecciones). Esto permite al compilador reutilizar el marco de cálculo en curso.
El compilador valida la rama TrueBranch: el tipo debe retornar una invocación pura del genérico sin spreads envolventes.
En lugar de generar un nuevo marco de pila, el compilador sobrescribe los registros locales de parámetros T y Acc.
Se incrementa el contador de iteraciones. Si no se superan los 1000 ciclos admisibles, la evaluación continúa.
Al alcanzarse el caso base de salida (Base Case), la estructura acumulada Acc se emite instantáneamente.
Construyamos un conjunto de herramientas con seguridad de tipos para trabajar con tuplas:
// 1. Extracción del primer elemento (Head)
export type Head<TTuple extends readonly unknown[]> = TTuple extends readonly [
infer THead,
...unknown[]
]
? THead
: never;
// 2. Extracción de la cola sin el primer elemento (Tail)
export type Tail<TTuple extends readonly unknown[]> = TTuple extends readonly [
unknown,
...infer TTail
]
? TTail
: [];
// 3. Añadir elemento al final de la tupla (Push)
export type Push<TTuple extends readonly unknown[], TElement> = [...TTuple, TElement];
// 4. Añadir elemento al principio de la tupla (Unshift)
export type Unshift<TTuple extends readonly unknown[], TElement> = [TElement, ...TTuple];
// 5. Extracción del último elemento (Last)
export type Last<TTuple extends readonly unknown[]> = TTuple extends readonly [
...unknown[],
infer TLast
]
? TLast
: never;
// 6. Eliminar el último elemento (Pop)
export type Pop<TTuple extends readonly unknown[]> = TTuple extends readonly [
...infer TRest,
unknown
]
? TRest
: [];
// Comprobaciones de prueba
type TestTuple = [string, number, boolean];
type T1 = Head<TestTuple>; // string
type T2 = Tail<TestTuple>; // [number, boolean]
type T3 = Push<TestTuple, Date>; // [string, number, boolean, Date]
type T4 = Last<TestTuple>; // boolean
type T5 = Pop<TestTuple>; // [string, number]Demostremos la potencia de la optimización por cola:
// ❌ IMPLEMENTACIÓN SIN TCO: ¡el compilador falla con tuplas de más de 45 elementos!
export type ReverseNaive<T extends readonly unknown[]> = T extends readonly [
infer Head,
...infer Tail
]
? [...ReverseNaive<Tail>, Head] // ¡El spread exterior [...] anula TCO!
: [];
// ✅ IMPLEMENTACIÓN CON TCO: ¡la llamada por cola con acumulador soporta hasta 1000 elementos!
export type Reverse<
TTuple extends readonly unknown[],
TAccumulator extends readonly unknown[] = []
> = TTuple extends readonly [infer THead, ...infer TTail]
? Reverse<TTail, [THead, ...TAccumulator]> // ¡Llamada por cola completamente limpia!
: TAccumulator;
// Verificación
type Original = ["a", "b", "c", "d"];
type Inverted = Reverse<Original>; // ["d", "c", "b", "a"]Curry<F>)#Una de las metas más avanzadas de la ingeniería de tipos: tipar funciones currificadas que admitan aplicación parcial sobre cualquier cantidad de argumentos:
// Currificador recursivo para tuplas de argumentos
export type Curry<TFunction> = TFunction extends (
...args: infer TArgs
) => infer TReturn
? TArgs extends [infer TFirst, ...infer TRest]
? TRest["length"] extends 0
? (arg: TFirst) => TReturn
: (arg: TFirst) => Curry<(...args: TRest) => TReturn>
: () => TReturn
: never;
// Demostración de currificación
function calculateOrderTotal(
basePrice: number,
taxRate: number,
discountCode: string,
isVip: boolean
): number {
const discount = isVip ? 0.2 : 0.05;
return basePrice * (1 + taxRate) * (1 - discount);
}
// Declaración runtime de función curry
declare function curry<F extends (...args: any[]) => any>(fn: F): Curry<F>;
const curriedOrder = curry(calculateOrderTotal);
// ¡El IDE proporciona autocompletado secuencial perfecto para cada paso!
const step1 = curriedOrder(1000); // (taxRate: number) => ...
const step2 = step1(0.2); // (discountCode: string) => ...
const step3 = step2("SUMMER2026"); // (isVip: boolean) => number
const finalPrice = step3(true); // numberDeepReadonly<T>#Recorrido recursivo de grafos de objetos anidados y tuplas para impedir mutaciones:
export type DeepReadonly<T> = T extends Function
? T
: T extends readonly (infer ElementType)[]
? ReadonlyArray<DeepReadonly<ElementType>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface ComplexMutableGraph {
rootId: string;
nodes: Array<{
id: number;
metadata: {
tags: string[];
owner: { name: string };
};
}>;
}
export type FrozenGraph = DeepReadonly<ComplexMutableGraph>;
declare const graph: FrozenGraph;
// ❌ Errores de compilación en todos los niveles de anidación:
// graph.rootId = "new-id"; // TS2540: Cannot assign to 'rootId'
// graph.nodes.push({ ... }); // TS2339: Property 'push' does not exist on 'ReadonlyArray'
// graph.nodes[0].metadata.tags[0] = "mutated"; // TS2542: Index signature in type 'readonly string[]' only permits readingpipe#Desarrollemos una canalización de composición de funciones de longitud arbitraria con verificación estática automática entre eslabones consecutivos:
export interface UnaryFunction<TIn, TOut> {
(input: TIn): TOut;
}
// Verificación de la cadena de funciones contra restricciones secuenciales
export type PipelineChain<TInput, TFunctions extends readonly unknown[]> =
TFunctions extends readonly [
UnaryFunction<infer TCurrentIn, infer TCurrentOut>,
...infer TRest
]
? [TInput] extends [TCurrentIn]
? [UnaryFunction<TCurrentIn, TCurrentOut>, ...PipelineChain<TCurrentOut, TRest>]
: never
: [];
// Tipo devuelto por la última función de la cadena
export type PipelineOutput<TInput, TFunctions extends readonly unknown[]> =
TFunctions extends readonly [
...unknown[],
UnaryFunction<any, infer TLastOut>
]
? TLastOut
: TInput;
export function createPipeline<TInitial>() {
return function execute<TFunctions extends readonly UnaryFunction<any, any>[]>(
initialValue: TInitial,
...fns: TFunctions & PipelineChain<TInitial, TFunctions>
): PipelineOutput<TInitial, TFunctions> {
return fns.reduce((acc, fn) => fn(acc), initialValue) as PipelineOutput<
TInitial,
TFunctions
>;
};
}
// Demostración de canalización estricta:
const runPipeline = createPipeline<string>();
const result = runPipeline(
" agenticlab_developer ",
(raw) => raw.trim(), // string => string
(clean) => clean.toUpperCase(), // string => string
(str) => str.length, // string => number
(len) => len > 10 // number => boolean
);
// ¡result tiene el tipo exacto 'boolean'!
// ❌ Error de compilación TS2345: intento de enlazar funciones incompatibles bloqueado:
/*
runPipeline(
"test",
(str) => str.length, // string => number
(num: boolean) => !num // ERROR: number no es asignable a boolean!
);
*/Si un tipo condicional carece de una rama terminal de salida (como comprobar T extends readonly []), el compilador no puede detenerse y agota la pila de instanciación inmediatamente con el error TS2589.
// ❌ RECURSIÓN INFINITA: sin comprobación de tupla vacía
type BadRecursive<T extends unknown[]> = T extends [infer Head, ...infer Tail]
? [Head, ...BadRecursive<Tail>]
: BadRecursive<T>; // ¡Bucle infinito fatal!
// ✅ CORRECTO: condición base terminal explícita
type GoodRecursive<T extends unknown[]> = T extends [infer Head, ...infer Tail]
? [Head, ...GoodRecursive<Tail>]
: []; // Finaliza en tupla vacíaDevolver [...TailRec<Rest>, Current] en vez de una invocación directa TailRec<Rest, [Current, ...Acc]> obliga al compilador a posponer la operación de propagación hasta el retorno. Esto anula la optimización TCO y reduce el límite de recursión de 1000 a unas 45 llamadas.
Pasar un array abierto string[] en lugar de una tupla [string, number] provoca que Head<string[]> evalúe a string y Tail<string[]> evalúe a string[], rompiendo por completo la descomposición secuencial de longitud.
Al desestructurar manualmente tuplas con elementos etiquetados [id: string, count: number], las operaciones simples de propagación pueden eliminar las etiquetas en las sugerencias del IDE. Mantén explícitamente las etiquetas si diseñas APIs públicas.
Se configura el acumulador inicial (por defecto una tupla vacía Acc = []).
Mediante [infer Head, ...infer Tail] se extrae la cabeza de la tupla.
El elemento extraído se incorpora al acumulador [Head, ...Acc].
El compilador sobrescribe el contexto local con los nuevos valores de Tail y Acc sin reservar memoria de pila.
Cuando la tupla de entrada queda vacía, la recursión finaliza retornando el acumulador Acc consolidado.