En el capítulo anterior viste cómo un conditional type elige entre dos tipos según una condición. Hay una extensión de esa idea que lo cambia todo: en vez de limitarte a elegir entre tipos que ya conoces, puedes capturar un tipo desconocido dentro de la condición y darle nombre. Esa es exactamente la función de infer.
infer: capturar un tipo desconocido#
La sintaxis aparece siempre dentro de la cláusula extends de un conditional type. En vez de escribir un tipo concreto, escribes infer seguido de un nombre, y TypeScript lo rellena con el tipo que encuentre en esa posición:
// ElementoDe<T> pregunta: "¿T es un array de algo?"
// Si lo es, infer U le pone nombre a ese "algo" y se devuelve en la rama true.
// Si no, el resultado es never.
type ElementoDe<T> = T extends Array<infer U> ? U : never;Cuando evalúas ElementoDe<Heroe[]>, TypeScript hace lo siguiente:
- Compara
Heroe[]con el patrónArray<infer U>. - Encaja:
Heroe[]esArray<Heroe>, así queUse captura comoHeroe. - La condición es verdadera, y se devuelve
U, es decir,Heroe.
Si pasas ElementoDe<number>, number no encaja con Array<infer U> —no es un array— así que la condición falla y el resultado es never.
// TypeScript resuelve cada uno en compilación. Pasa el ratón por encima.
type A = ElementoDe<Heroe[]>; // Heroe
type B = ElementoDe<string[]>; // string
type C = ElementoDe<number>; // neverinfer solo es válido en la condición#
Una restricción importante: infer solo funciona dentro de la cláusula extends de un conditional type. No puedes usarlo en otro lugar. Si lo intentas fuera de esa posición, TypeScript lanza un error.
// Esto es VÁLIDO: infer U aparece dentro de la condición del extends.
type ElementoDe<T> = T extends Array<infer U> ? U : never;
// Esto NO compila: infer U no está dentro de un conditional type.
// type Mal<T> = infer U;La razón es conceptual: infer captura un tipo que solo existe si la condición se cumple. Fuera de esa condición, no hay nada que capturar.
ReturnType por dentro: ya no es magia#
En el Nivel 5 usaste ReturnType<typeof miFuncion> como una caja negra. Ahora puedes ver qué hay dentro:
// La condición (...args: any[]) => infer R encaja con CUALQUIER función.
// args: any[] acepta cualquier número de parámetros, de cualquier tipo.
// Lo que infer R captura es lo que viene después de =>: el tipo de retorno.
type RetornoDe<T> = T extends (...args: any[]) => infer R ? R : never;Cuando evalúas RetornoDe<() => Heroe>:
- TypeScript compara
() => Heroecon el patrón(...args: any[]) => infer R. - Encaja.
Rse captura comoHeroe. - Se devuelve
R, es decir,Heroe.
Así está implementado ReturnType en la librería estándar de TypeScript, sin ninguna magia especial del compilador.
El mismo patrón aplicado a Promise#
Una vez que entiendes el patrón, aplicarlo a otras formas es mecánico. Una Promise<T> es una caja que contiene un tipo; infer la abre:
// PromesaDe<T> pregunta: "¿T es una Promise<algo>?"
// Si lo es, infer V captura ese algo y se devuelve.
// Si T no es una Promise, never.
type PromesaDe<T> = T extends Promise<infer V> ? V : never;Esto es la base de cómo funciona el utility Awaited de la librería estándar (la versión completa además maneja Promises anidadas, algo que verás más adelante con tipos recursivos).
Puedes combinar RetornoDe y PromesaDe en cadena para extraer el tipo que devuelve una función asíncrona:
// cargarEquipo devuelve Promise<Heroe[]>.
declare function cargarEquipo(): Promise<Heroe[]>;
// Paso 1: extraer el tipo de retorno de la función → Promise<Heroe[]>.
type Retorno = RetornoDe<typeof cargarEquipo>;
// Paso 2: abrir la Promise → Heroe[].
type TipoDelEquipo = PromesaDe<Retorno>;Lo que acabas de aprender#
infer es el mecanismo con el que la librería estándar de TypeScript implementa sus utilities más potentes. ReturnType, Parameters, Awaited, ConstructorParameters: todos usan infer para abrir una caja de tipo desconocido y sacar lo que hay dentro. Ahora que entiendes el patrón, esas utilidades dejan de ser magia y pasan a ser herramientas que puedes leer, reproducir y adaptar.
Comprueba lo que sabes#
Pregunta 1 de 4
¿En qué posición es válida la palabra clave `infer`?
Tu turno#
Implementa los tres extractores del Team Builder hasta que los tests Expect<Equal<...>> queden sin subrayado rojo. Empiezas con el extractor de array y acabas reconstruyendo los patrones internos de ReturnType y Awaited. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.
Ejercicio · en esta página
Construye extractores de tipos con infer para el Team Builder
Implementa tres tipos que usan infer para extraer información de otros tipos. Trabajas a nivel de tipos: completa los TODO hasta que los tests Expect<Equal<...>> queden sin subrayado rojo.
Paso 1: El primer infer
- ElementoDe<T> extrae el tipo de los elementos de un array con infer U.
- ElementoDe<Heroe[]> da Heroe; ElementoDe<string[]> da string; ElementoDe<number> da never.
- Los tres primeros tests en verde.
Paso 2: ReturnType reconstruido
- RetornoDe<T> extrae el tipo de retorno de cualquier función con infer R.
- Reconoces que es, literalmente, el utility ReturnType reconstruido a mano.
- Sus dos tests en verde.
Paso 3: Tres utilities reales
- PromesaDe<T> extrae el tipo dentro de una Promise con infer V.
- Reconoces que has reconstruido los patrones internos de ReturnType y Awaited.
- Los siete tests en verde.
Ver soluciones
// Helper de testing de tipos (lo viste en conditional types; se explica a fondo en
// "Testear a nivel de tipos"). Expect<Equal<A, B>> da error si A y B no son iguales.
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: "Daño" | "Tanque" | "Apoyo";
partidas: number;
victorias: number;
}
// ElementoDe<T>: la primera aplicación de infer.
// El conditional pregunta: "¿T es un Array de algo?" Si lo es, ese "algo" se captura
// con infer U y se devuelve. Si T no es un array en absoluto, el resultado es never.
// Así: ElementoDe<Heroe[]> → Heroe ; ElementoDe<string[]> → string ; ElementoDe<number> → never.
type ElementoDe<T> = T extends Array<infer U> ? U : never;
// RetornoDe y PromesaDe quedan pendientes (los tiers siguientes los añaden).
type RetornoDe<T> = unknown;
type PromesaDe<T> = unknown;
declare function obtenerHeroe(): Heroe;
declare function obtenerNombre(): string;
declare function cargarEquipo(): Promise<Heroe[]>;
// Tests del tier ok en verde (los de RetornoDe y PromesaDe siguen en rojo):
type _T1 = Expect<Equal<ElementoDe<Heroe[]>, Heroe>>;
type _T2 = Expect<Equal<ElementoDe<string[]>, string>>;
type _T3 = Expect<Equal<ElementoDe<number>, never>>;
console.log("ElementoDe correcto: infer U captura el tipo de los elementos."); Por qué este nivel
- ElementoDe<T> es la forma más directa de ver infer en acción: la condición pregunta si T es un array, y si lo es, infer U captura el tipo de sus elementos para devolverlo.
- El patrón T extends Array<infer U> ? U : never es tan común que es casi la definición canónica de infer: ves una caja, metes la mano y sacas lo que hay dentro.
- Funciona, pero RetornoDe y PromesaDe siguen sin implementar: el nivel siguiente aplica el mismo patrón a funciones.
// Helper de testing de tipos (lo viste en conditional types; se explica a fondo en
// "Testear a nivel de tipos"). Expect<Equal<A, B>> da error si A y B no son iguales.
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: "Daño" | "Tanque" | "Apoyo";
partidas: number;
victorias: number;
}
// ElementoDe<T>: infer U captura el tipo de los elementos de un array.
type ElementoDe<T> = T extends Array<infer U> ? U : never;
// RetornoDe<T>: reconstruye el utility ReturnType de la librería estándar.
// La condición pregunta: "¿T es una función (de cualquier argumentos) que devuelve algo?"
// Si lo es, infer R captura ese algo. Si T no es una función en absoluto, never.
// (...args: any[]) en la condición acepta funciones de cualquier número de parámetros;
// lo que importa capturar es lo que viene después de =>: el tipo de retorno R.
// Así: RetornoDe<() => string> → string ; RetornoDe<(n: number) => Heroe> → Heroe.
type RetornoDe<T> = T extends (...args: any[]) => infer R ? R : never;
// PromesaDe queda pendiente (el tier excelente lo añade).
type PromesaDe<T> = unknown;
declare function obtenerHeroe(): Heroe;
declare function obtenerNombre(): string;
declare function cargarEquipo(): Promise<Heroe[]>;
// Tests de los tiers ok y mejor en verde (el de PromesaDe sigue en rojo):
type _T1 = Expect<Equal<ElementoDe<Heroe[]>, Heroe>>;
type _T2 = Expect<Equal<ElementoDe<string[]>, string>>;
type _T3 = Expect<Equal<ElementoDe<number>, never>>;
type _T4 = Expect<Equal<RetornoDe<typeof obtenerHeroe>, Heroe>>;
type _T5 = Expect<Equal<RetornoDe<typeof obtenerNombre>, string>>;
console.log("ElementoDe y RetornoDe correctos: ReturnType ya no es magia."); Por qué es mejor que el anterior
- RetornoDe<T> usa (...args: any[]) => infer R como condición: cualquier función encaja, sean cuales sean sus parámetros. Lo único que captura infer R es el tipo que viene después de =>, el retorno.
- Es el utility ReturnType de la librería estándar, reconstruido línea a línea. Ya no es una caja negra: sabes exactamente qué hace por dentro.
- El salto a excelente es aplicar el mismo patrón una vez más, esta vez a una Promise en vez de a una función.
// Helper de testing de tipos (lo viste en conditional types; se explica a fondo en
// "Testear a nivel de tipos"). Expect<Equal<A, B>> da error si A y B no son iguales.
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: "Daño" | "Tanque" | "Apoyo";
partidas: number;
victorias: number;
}
// ElementoDe<T>: infer U captura el tipo de los elementos de un array.
type ElementoDe<T> = T extends Array<infer U> ? U : never;
// RetornoDe<T>: reconstruye ReturnType. infer R captura el tipo de retorno de la función.
type RetornoDe<T> = T extends (...args: any[]) => infer R ? R : never;
// PromesaDe<T>: reconstruye la capa superficial de Awaited.
// La condición pregunta: "¿T es una Promise<algo>?" Si lo es, infer V captura ese algo.
// Si T no es una Promise en absoluto, never.
// El utility Awaited de la librería estándar hace lo mismo pero además maneja Promises
// anidadas y thenables de forma recursiva; esta versión solo desenvuelve un nivel.
// Así: PromesaDe<Promise<Heroe[]>> → Heroe[] ; PromesaDe<string> → never.
type PromesaDe<T> = T extends Promise<infer V> ? V : never;
declare function obtenerHeroe(): Heroe;
declare function obtenerNombre(): string;
declare function cargarEquipo(): Promise<Heroe[]>;
// Todos los tests en verde:
type _T1 = Expect<Equal<ElementoDe<Heroe[]>, Heroe>>;
type _T2 = Expect<Equal<ElementoDe<string[]>, string>>;
type _T3 = Expect<Equal<ElementoDe<number>, never>>;
type _T4 = Expect<Equal<RetornoDe<typeof obtenerHeroe>, Heroe>>;
type _T5 = Expect<Equal<RetornoDe<typeof obtenerNombre>, string>>;
type _T6 = Expect<Equal<PromesaDe<Promise<Heroe[]>>, Heroe[]>>;
type _T7 = Expect<Equal<PromesaDe<string>, never>>;
console.log("ElementoDe, RetornoDe y PromesaDe correctos: tres utilities reales reconstruidos con infer."); Por qué es mejor que el anterior
- PromesaDe<T> cierra el círculo: T extends Promise<infer V> captura el tipo que está dentro de la Promise, igual que los anteriores captureaban el elemento del array o el retorno de la función.
- En tres tipos has reconstruido los patrones internos de ReturnType y de la capa superficial de Awaited. No son magia del compilador: son infer aplicado a diferentes formas.
- En un proyecto real usarías los utilities de TypeScript directamente; ahora entiendes qué hacen por dentro, y eso es lo que separa usarlos de comprenderlos.