Entendiendo `infer` en TypeScript
infer declara una variable de tipo dentro de la cláusula extends de un tipo condicional, capturando un subtipo coincidente para que pueda utilizarse en la rama verdadera — no tiene significado fuera de esa posición y constituye un error de compilación en cualquier otro lugar. Si alguna vez has abierto el archivo .d.ts de una librería o el PR de un colega y te has encontrado con algo como T extends (...args: any[]) => infer R ? R : any, esa línea usa infer para extraer una parte de un tipo de la misma manera en que la desestructuración extrae un valor de un objeto.
Puntos clave
infersolo es válido dentro de la cláusulaextendsde un tipo condicional, y la variable que introduce solo está en ámbito en la rama verdadera — referenciarla en la rama falsa es un error del compilador.inferfue introducido en TypeScript 2.8;infercon literales de plantilla llegó en la versión 4.1,Awaited<T>en la 4.5, einfer X extends Constrainten la 4.7.- Cuando el parámetro de tipo es una variable de tipo desnuda y se pasa una unión, el condicional se distribuye sobre cada miembro, por lo que
inferse resuelve una vez por miembro y los resultados se acumulan en una unión — un error silencioso de ampliación de tipos muy común. - Desde TypeScript 4.5,
Awaited<T>es la utilidad incorporada para desenvolver cadenas dePromise; no hay razón para reimplementarUnwrapPromisemanualmente. - Múltiples candidatos para la misma variable
inferproducen una unión en posiciones covariantes y una intersección en posiciones contravariantes.
Tipos condicionales: el sustrato donde vive infer
Un tipo condicional selecciona uno de dos tipos según si un tipo es asignable a otro, usando la forma T extends X ? A : B. Funciona como un operador ternario, pero a nivel de tipos: si T es asignable a X, el tipo se resuelve como A; de lo contrario, como B. El manual de TypeScript sobre tipos condicionales es la referencia principal.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
El comportamiento que suele confundir a la gente es la distributividad. Cuando el tipo verificado es un parámetro de tipo desnudo — T directamente, sin envolver —, y se pasa una unión, TypeScript distribuye el condicional sobre cada miembro de la unión por separado y acumula los resultados en una nueva unión. Envolver el parámetro en una tupla ([T] extends [X]) desactiva la distribución y verifica la unión como una unidad. Esta distinción está documentada en tipos condicionales distributivos.
type Distributed<T> = T extends string ? T[] : never;
type R1 = Distributed<string | number>; // string[] (el miembro number se resuelve como never y se elimina)
type NonDistributed<T> = [T] extends [string] ? T[] : never;
type R2 = NonDistributed<string | number>; // never — se verifica como una unidad
Mantén esta regla presente: es la raíz de la sorpresa más común con infer, que se aborda en la sección de errores frecuentes más adelante.
Anatomía de un patrón con infer
Discover how at OpenReplay.com.
Una variable de tipo introducida con infer solo está en ámbito en la rama verdadera del tipo condicional; referenciarla en la rama falsa es un error del compilador de TypeScript. infer introduce la variable dentro de un patrón extends y la vincula a lo que coincida con esa posición cuando se evalúa el condicional. Esto hace que infer se comporte de manera similar a la desestructuración — pero para tipos: se escribe un patrón que refleja la forma esperada, se nombran las partes que se desean extraer, y TypeScript las completa.
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // number
El patrón (infer U)[] indica “un array de algún tipo de elemento — llamemos a ese tipo de elemento U.” Cuando T es string[], U se vincula a string. Cuando la coincidencia falla, se ejecuta la rama falsa, y U ya no está disponible allí:
type Bad<T> = T extends (infer U)[] ? U[] : U;
// ^ Error: Cannot find name 'U'.
Un solo patrón puede contener múltiples variables infer, cada una vinculada a su propia posición:
type SplitFn<T> = T extends (arg: infer A) => infer R ? [A, R] : never;
type S = SplitFn<(x: number) => string>; // [number, string]
Aquí infer A captura el tipo del parámetro e infer R captura el tipo de retorno en un solo paso. Esta es exactamente la técnica que utilizan los tipos utilitarios incorporados.
infer en la biblioteca estándar: los extractores canónicos
La biblioteca estándar de TypeScript implementa sus utilidades de extracción con infer. Cada una sigue la misma estructura: coincidir con un patrón, vincular la posición de interés y devolverla en la rama verdadera. Las definiciones que se muestran a continuación son las que se distribuyen en lib/es5.d.ts.
Tipo de retorno de una función — ReturnType
ReturnType<T> extrae el tipo de retorno de un tipo función haciendo coincidir la forma de la función y vinculando la posición de retorno a infer R. La definición de la biblioteca estándar restringe T para que sea invocable.
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
type A = ReturnType<() => string>; // string
type B = ReturnType<() => { id: number }>; // { id: number }
Nótese la restricción genérica T extends (...args: any) => any — forma parte de la definición real de la biblioteca estándar, no es una decoración opcional.
Conclusión: ReturnType es el ejemplo más claro de infer — se hace coincidir la función con el patrón y se captura la posición de retorno.
Parámetros de una función como tupla — Parameters
Parameters<T> extrae la lista de parámetros de una función como una tupla, vinculando la posición del parámetro rest a infer P.
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type Args = Parameters<(a: string, b: number) => void>; // [a: string, b: number]
Conclusión: Dado que los parámetros se modelan como un tipo tupla, infer P sobre ...args proporciona la lista completa, preservando las etiquetas y la opcionalidad.
Resolución de Promises — usa Awaited<T>, no un desenvolvedor artesanal
Desde TypeScript 4.5, el Awaited<T> incorporado desenvuelve recursivamente cadenas de Promise y maneja PromiseLike — no hay razón para reimplementar UnwrapPromise manualmente en TypeScript moderno. Muchos tutoriales más antiguos definen su propio T extends Promise<infer U> ? U : T, pero esa versión de un solo nivel no desenvuelve promesas anidadas ni modela objetos thenable. Awaited hace ambas cosas, como se describe en las notas de la versión TypeScript 4.5.
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number — recursivo
type C = Awaited<boolean | Promise<number>>; // number | boolean
Conclusión: Usa Awaited<T> directamente. Escribe tu propio desenvolvedor de promesas basado en infer solo como ejercicio de aprendizaje.
Tipo de elemento de un array
Extraer el tipo de elemento de un array es el patrón infer más pequeño y útil: hacer coincidir (infer U)[] y devolver U.
type ElementOf<T> = T extends readonly (infer U)[] ? U : never;
type A = ElementOf<number[]>; // number
type B = ElementOf<readonly string[]>; // string
Conclusión: Incluye readonly en el patrón para que la utilidad funcione tanto con arrays mutables como de solo lectura.
Cabeza, cola y resto de una tupla
Las tuplas admiten patrones infer posicionales con elementos rest, lo que permite extraer la cabeza, la cola o el último elemento. Los patrones reflejan la desestructuración de arrays en JavaScript.
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type H = Head<[1, 2, 3]>; // 1
type R = Tail<[1, 2, 3]>; // [2, 3]
type L = Last<[1, 2, 3]>; // 3
Conclusión: [infer Head, ...infer Rest] y [...any[], infer Last] son los bloques de construcción para la manipulación recursiva de tuplas — los mismos patrones impulsan routers a nivel de tipos y librerías de estado de formularios.
Inferencia recursiva — Flatten
infer combinado con recursión permite que un tipo desenrede estructuras arbitrariamente profundas. Flatten recursa a través de arrays anidados hasta llegar a un tipo que no sea un array.
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
Cada pasada vincula U al tipo de elemento y lo vuelve a pasar a Flatten. Cuando T ya no es un array, la rama falsa lo devuelve sin cambios.
Conclusión: infer recursivo es la manera de colapsar contenedores anidados; la misma estructura desenvuelve tipos de Promise u objetos profundamente anidados.
Inferencia con literales de plantilla — análisis de cadenas a nivel de tipos
Los tipos de literales de plantilla admiten infer dentro de sus posiciones de sustitución. El ejemplo ExtractId<T> que se muestra a continuación hace coincidir el patrón de cadena y vincula el segmento variable a Id, permitiendo la extracción tipada de parámetros de ruta sin un analizador en tiempo de ejecución — un patrón disponible desde TypeScript 4.1.
type ExtractId<T> = T extends `/users/${infer Id}` ? Id : never;
type A = ExtractId<"/users/42">; // "42"
type B = ExtractId<"/posts/42">; // never
Se pueden encadenar variables infer a través de múltiples segmentos para analizar una ruta completa en sus partes:
type RouteParams<T> =
T extends `${infer _Start}/:${infer Param}/${infer Rest}`
? Param | RouteParams<`/${Rest}`>
: T extends `${infer _Start}/:${infer Param}`
? Param
: never;
type P = RouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"
Este es el equivalente a nivel de tipos de un analizador de rutas, y es la base del enrutamiento tipado en frameworks y librerías que derivan objetos de parámetros a partir de cadenas de ruta.
Conclusión: infer con literales de plantilla convierte tipos con forma de cadena en datos estructurados con costo cero en tiempo de ejecución — útil para parámetros de ruta, uniones de propiedades CSS y análisis de formatos de cadenas con marca.
Inferencia con restricciones: infer X extends Constraint (TS 4.7)
TypeScript 4.7 añadió infer X extends Constraint, permitiendo que un tipo condicional restrinja una variable inferida en el momento de la coincidencia — combinando inferencia y verificación de restricciones en un solo paso. Esto está documentado en las notas de la versión TypeScript 4.7. Antes de la versión 4.7, era necesario inferir primero y luego añadir un segundo condicional anidado para estrechar el resultado.
// Antes de 4.7: inferir y luego verificar en un segundo condicional
type FirstStringOld<T> =
T extends [infer H, ...any[]]
? H extends string ? H : never
: never;
// 4.7+: restringir en el momento de la inferencia
type FirstString<T> =
T extends [infer H extends string, ...any[]] ? H : never;
type A = FirstString<["hi", 1, 2]>; // "hi"
type B = FirstString<[1, 2, 3]>; // never
TypeScript 4.8 mejoró posteriormente la inferencia con restricciones para tipos primitivos como number, bigint y boolean, permitiendo al compilador preservar tipos literales más precisos al hacer coincidir patrones de cadenas de plantilla. Esto se apoya en la sintaxis de infer con restricciones introducida en TypeScript 4.7, pero fue entregado como una mejora independiente.
Conclusión: Usa infer X extends C cuando quieras tanto el tipo capturado como una garantía sobre su forma en una sola expresión — reemplaza el patrón más antiguo de inferir y luego anidar.
Tres errores frecuentes que rompen la inferencia silenciosamente
Los tres errores frecuentes que rompen infer de forma silenciosa son: la distributividad sobre uniones que amplía los resultados, la varianza que alterna los múltiples candidatos de infer entre unión e intersección, y los patrones demasiado específicos que caen en la rama falsa. Los fallos de infer suelen ser silenciosos — el tipo sigue compilando, pero es más amplio o más estrecho de lo que se pretendía.
1. La distributividad amplía los resultados silenciosamente
Cuando el parámetro de tipo T es una variable de tipo desnuda y se pasa una unión, TypeScript distribuye el condicional sobre cada miembro por separado — lo que significa que infer R se resuelve una vez por miembro de la unión y los resultados se acumulan en una unión, lo que puede ampliar silenciosamente el tipo inferido más allá de lo que se pretendía.
type Unwrap<T> = T extends Promise<infer U> ? U : T;
// Parece que devuelve un solo tipo; en realidad distribuye
type R = Unwrap<Promise<string> | Promise<number>>; // string | number
Si se quiere rechazar uniones mixtas en lugar de colapsarlas, hay que envolver el parámetro para desactivar la distribución:
type UnwrapStrict<T> = [T] extends [Promise<infer U>] ? U : T;
Conclusión: Una T desnuda más una unión implica evaluación por miembro. Envuélvela en [T] cuando necesites que la unión se trate como una unidad.
2. La posición covariante vs. contravariante cambia el resultado
Múltiples candidatos para la misma variable infer producen una unión en posiciones covariantes (p. ej., tipos de retorno) y una intersección en posiciones contravariantes (p. ej., tipos de parámetros de función). Para funciones sobrecargadas, la inferencia utiliza la última firma. Este comportamiento está descrito en las notas de la versión TypeScript 2.8.
// Covariante (posición de retorno): unión
type Co<T> = T extends { a: () => infer U; b: () => infer U } ? U : never;
type C = Co<{ a: () => string; b: () => number }>; // string | number
// Contravariante (posición de parámetro): intersección
type Contra<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type D = Contra<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
Conclusión: Reutilizar un mismo nombre infer en varias posiciones es intencional y poderoso, pero si se obtiene una unión o una intersección depende de la varianza de las posiciones. No asumas que siempre será una unión.
3. Los patrones que no pueden vincularse caen en la rama falsa
infer solo se vincula cuando el tipo candidato coincide estructuralmente con el patrón. Si la forma no coincide, el condicional toma la rama falsa y la variable inferida nunca se resuelve — por lo que un patrón demasiado específico devuelve silenciosamente el tipo de reserva en lugar del valor esperado.
type FirstArg<T> = T extends (a: infer A, b: infer B) => any ? A : never;
type X = FirstArg<string>; // never — string no coincide con un tipo función
En este ejemplo, FirstArg espera un tipo función. Pasar string no coincide con ese patrón, por lo que el condicional se resuelve en la rama falsa y devuelve never.
Conclusión: Haz los patrones tan flexibles como permita la coincidencia. Usa ...args: infer P o [infer H, ...any[]] en lugar de formas de aridad fija, a menos que específicamente quieras rechazar otras aridades.
Dónde te encuentras con infer en la práctica
En el código de aplicación raramente escribes infer tú mismo — lo encuentras en definiciones de tipos de librerías como ComponentProps de React, RequestHandler de Express, PayloadAction de Redux Toolkit y los tipos de endpoints de RTK Query, y utilizas los tipos utilitarios resultantes. Reconocer el patrón T extends Wrapper<infer X> ? X : Fallback te permite leer esas definiciones en lugar de tratarlas como cajas negras.
ComponentProps de React. Las tipificaciones de @types/react definen ComponentProps con infer para extraer las props de un componente a partir de su tipo, ramificando entre elementos del host y constructores de componentes. Al leerlo, se puede ver la misma forma T extends ... infer P ... ? P : ... de ReturnType, aplicada a los tipos de elementos de React.
Manejadores de solicitudes de Express. El paquete @types/express de Express modela RequestHandler como un genérico con parámetros para los parámetros de ruta, el cuerpo de la respuesta, el cuerpo de la solicitud y la query. Las librerías que derivan manejadores de ruta tipados a partir de una cadena de ruta combinan estos genéricos con infer de literales de plantilla para extraer los segmentos :param — el patrón RouteParams mostrado anteriormente es el núcleo de esa maquinaria.
PayloadAction de Redux Toolkit. PayloadAction de Redux Toolkit es un tipo útil para extraer con infer, aunque su propia definición se basa en genéricos y tipos condicionales en lugar de infer internamente — un recordatorio de que infer es la manera en que tú lees los tipos de una librería, no necesariamente cómo está escrita la librería. Comúnmente escribirás type Payload<A> = A extends PayloadAction<infer P> ? P : never para recuperar el tipo de payload de un reducer a partir de su acción.
import type { PayloadAction } from "@reduxjs/toolkit";
type PayloadOf<A> = A extends PayloadAction<infer P> ? P : never;
type P = PayloadOf<PayloadAction<{ id: string }>>; // { id: string }
Endpoints de RTK Query. RTK Query (parte de Redux Toolkit) expone tipos de endpoints generados donde los tipos de consulta y resultado son recuperables con utilidades basadas en infer. Extraer el tipo de resultado de un endpoint con un condicional que vincula la posición del resultado es una tarea rutinaria una vez que se reconoce el patrón.
Conclusión
infer es una característica con una sola regla: nombra una posición dentro de un patrón de tipo condicional y vincula lo que coincida allí, utilizable solo en la rama verdadera. Una vez que veas T extends Pattern<infer X> ? X : Fallback como desestructuración a nivel de tipos, cada utilidad incorporada y tipificación de librería se lee como una variación de ese único movimiento. La próxima vez que te encuentres con infer en un archivo .d.ts, traza el patrón: encuentra la posición en la que se sitúa la variable, verifica si el parámetro está desnudo o envuelto, y confirma que estás leyendo la rama verdadera. Comienza reemplazando cualquier UnwrapPromise artesanal en tu base de código con Awaited<T>, y adopta infer X extends Constraint donde actualmente estés anidando un segundo condicional para estrechar un resultado.
Preguntas frecuentes
infer extrae un tipo de un tipo existente mediante coincidencia de patrones dentro de un condicional, mientras que un tipo mapeado transforma cada propiedad de un tipo en una nueva forma. Usa infer cuando necesites leer un subtipo de una estructura, como el tipo de retorno de una función o el tipo de elemento de un array. Usa un tipo mapeado cuando necesites iterar sobre claves y producir un nuevo tipo de objeto. Resuelven problemas opuestos: infer lee, los tipos mapeados reescriben.
No. Una variable de tipo introducida con infer solo está en ámbito en la rama verdadera del tipo condicional; referenciarla en la rama falsa produce un error del compilador de TypeScript como 'Cannot find name'. La variable solo existe una vez que la coincidencia tiene éxito, por lo que la rama falsa no tiene ningún valor que vincular. Si necesitas un valor en ambos resultados, infiere en la rama verdadera y proporciona un tipo de reserva independiente en la rama falsa.
Por la distributividad. Cuando el parámetro de tipo es una variable de tipo desnuda y se pasa una unión, TypeScript distribuye el condicional sobre cada miembro por separado, por lo que infer se resuelve una vez por miembro de la unión y los resultados se acumulan en una nueva unión. Esto puede ampliar silenciosamente el resultado más allá de lo que se pretendía. Para desactivar la distribución y verificar la unión como una unidad, envuelve el parámetro en una tupla, como en [T] extends [Promise<infer U>] ? U : T.
No, no en TypeScript moderno. Desde TypeScript 4.5, el Awaited<T> incorporado desenvuelve recursivamente cadenas de Promise y maneja objetos thenables PromiseLike. Un T extends Promise<infer U> ? U : T artesanal solo desenvuelve un nivel e ignora los objetos then-able, por lo que falla con promesas anidadas. Usa Awaited<T> directamente en el código de aplicación, y escribe tu propio desenvolvedor basado en infer solo como ejercicio de aprendizaje.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.