Sintaxis de los tipos condicionales
Los tipos condicionales siguen la estructura T extends U ? X : Y, que funciona como un operador ternario a nivel de tipos. Si el tipo T es asignable al tipo U, el resultado es X; en caso contrario es Y.

type EsString<T> = T extends string ? true : false
type A = EsString<string> // true
type B = EsString<number> // false
type C = EsString<"hola"> // true (los literales de string extienden string)
La potencia de los tipos condicionales aparece al combinarlos con genéricos, creando utilidades de transformación reutilizables:
type EsArray<T> = T extends any[] ? true : false
type D = EsArray<string[]> // true
type E = EsArray<number> // false
type F = EsArray<[1, 2]> // true (las tuplas extienden any[])
La cláusula
extendsen tipos condicionales no significa herencia de clases. Significa asignabilidad: si un valor de tipoTse puede asignar a una variable de tipoU, entoncesT extends Ues verdadero.
Tipos condicionales con múltiples ramas
Se pueden anidar tipos condicionales para crear ramas múltiples:
type NombreTipo<T> =
T extends string ? "texto" :
T extends number ? "número" :
T extends boolean ? "booleano" :
T extends undefined ? "indefinido" :
T extends null ? "nulo" :
T extends Function ? "función" :
"objeto"
type R1 = NombreTipo<string> // "texto"
type R2 = NombreTipo<42> // "número"
type R3 = NombreTipo<true> // "booleano"
type R4 = NombreTipo<() => void> // "función"
type R5 = NombreTipo<{ a: 1 }> // "objeto"
Tipos condicionales para transformación
type Aplanar<T> = T extends any[] ? T[number] : T
type G = Aplanar<string[]> // string
type H = Aplanar<number[][]> // number[]
type I = Aplanar<boolean> // boolean
type Envolver<T> = T extends any ? T[] : never
type J = Envolver<string> // string[]
type K = Envolver<number | string> // number[] | string[] (distributivo)
Tipos condicionales distributivos
Cuando el tipo T de un condicional es un parámetro genérico desnudo (sin envolver en otro tipo), TypeScript aplica el condicional de forma distributiva sobre cada miembro de una unión:
type EnArray<T> = T extends any ? T[] : never
// Se aplica a cada miembro por separado:
type L = EnArray<string | number>
// EnArray<string> | EnArray<number>
// string[] | number[]
Sin distributividad, el resultado sería diferente:
// Desactivar distributividad envolviendo en tupla
type EnArrayNoDistributivo<T> = [T] extends [any] ? T[] : never
type M = EnArrayNoDistributivo<string | number>
// (string | number)[]
La diferencia es significativa:
string[] | number[]solo admite arrays puros de un tipo, mientras que(string | number)[]admite arrays mixtos. La distributividad se desactiva envolviendo ambos lados delextendsentre corchetes[].
Filtrar miembros de una unión
La distributividad permite filtrar miembros de una unión usando never:
type SoloStrings<T> = T extends string ? T : never
type N = SoloStrings<"a" | 1 | "b" | true | "c">
// "a" | "b" | "c"
Este es exactamente el mecanismo que usa Extract:
// Implementación de Extract
type MiExtract<T, U> = T extends U ? T : never
// Implementación de Exclude
type MiExclude<T, U> = T extends U ? never : T
type O = MiExtract<string | number | boolean, string | boolean>
// string | boolean
type P = MiExclude<string | number | boolean, string>
// number | boolean
Distributividad con tipos complejos
type ExtraerFunciones<T> = T extends (...args: any[]) => any ? T : never
type Mixto = string | (() => void) | number | ((x: number) => string)
type SoloFunciones = ExtraerFunciones<Mixto>
// (() => void) | ((x: number) => string)
type NoNulo<T> = T extends null | undefined ? never : T
type Q = NoNulo<string | null | number | undefined>
// string | number
La palabra clave infer
La palabra clave infer permite que TypeScript capture un tipo de una posición específica dentro de la cláusula extends de un tipo condicional. Solo puede usarse dentro de esa cláusula.
type ElementoArray<T> = T extends (infer E)[] ? E : T
type R = ElementoArray<string[]> // string
type S = ElementoArray<number[]> // number
type T1 = ElementoArray<boolean> // boolean (no es array, devuelve T)
La diferencia con el acceso indexado es que infer es más declarativo y permite capturar tipos en posiciones donde el acceso indexado no funcionaria:
// Sin infer: funciona pero menos flexible
type ElementoSinInfer<T> = T extends any[] ? T[number] : T
// Con infer: más expresivo y extensible
type ElementoConInfer<T> = T extends (infer E)[] ? E : T
infer con funciones
Extraer tipos de funciones es uno de los usos más frecuentes de infer. Los utility types ReturnType y Parameters están implementados con esta técnica:
// Extraer tipo de retorno
type MiReturnType<T> = T extends (...args: any[]) => infer R ? R : never
// Extraer tipos de parámetros
type MiParameters<T> = T extends (...args: infer P) => any ? P : never
function procesar(nombre: string, cantidad: number): { ok: boolean; total: number } {
return { ok: true, total: cantidad }
}
type Retorno = MiReturnType<typeof procesar>
// { ok: boolean; total: number }
type Params = MiParameters<typeof procesar>
// [string, number]
Extraer el primer parámetro
type PrimerParametro<T> = T extends (primero: infer P, ...resto: any[]) => any
? P
: never
type PP = PrimerParametro<(a: string, b: number) => void>
// string
Extraer el tipo this
type TipoThis<T> = T extends (this: infer U, ...args: any[]) => any ? U : never
function método(this: { nombre: string }, saludo: string): string {
return `${saludo}, ${this.nombre}`
}
type ThisDelMetodo = TipoThis<typeof método>
// { nombre: string }
infer con promesas
Desempaquetar el tipo de una Promise es otra aplicación clásica de infer:
type DesempaquetarPromesa<T> = T extends Promise<infer U> ? U : T
type V = DesempaquetarPromesa<Promise<string>>
// string
type W = DesempaquetarPromesa<Promise<{ id: number; nombre: string }>>
// { id: number; nombre: string }
type X = DesempaquetarPromesa<number>
// number (no es Promise, devuelve T)
Para desempaquetar promesas anidadas de forma recursiva:
type DesempaquetarProfundo<T> = T extends Promise<infer U>
? DesempaquetarProfundo<U>
: T
type Y = DesempaquetarProfundo<Promise<Promise<Promise<number>>>>
// number
Este patrón recursivo es esencialmente lo que hace el utility type
Awaited<T>de la biblioteca estándar, con una implementación más robusta que maneja objetos thenable.
infer con clases y constructores
type TipoInstancia<T> = T extends new (...args: any[]) => infer I ? I : never
class Conexion {
constructor(public host: string, public puerto: number) {}
}
type InstConexion = TipoInstancia<typeof Conexion>
// Conexion
type ParamsConstructor<T> = T extends new (...args: infer P) => any ? P : never
type ArgsConexion = ParamsConstructor<typeof Conexion>
// [string, number]
Múltiples infer en un tipo condicional
Se pueden usar varias declaraciones infer en la misma cláusula condicional para capturar distintas partes de un tipo:
type PrimeroYUltimo<T> = T extends [infer Primero, ...any[], infer Ultimo]
? { primero: Primero; ultimo: Ultimo }
: never
type Z = PrimeroYUltimo<[string, number, boolean, Date]>
// { primero: string; ultimo: Date }
type Z2 = PrimeroYUltimo<[number, string]>
// { primero: number; ultimo: string }
Separar cabeza y cola de una tupla
type Cabeza<T> = T extends [infer H, ...any[]] ? H : never
type Cola<T> = T extends [any, ...infer R] ? R : never
type H1 = Cabeza<[string, number, boolean]> // string
type C1 = Cola<[string, number, boolean]> // [number, boolean]
Extraer clave y valor de una entrada
type ExtraerEntrada<T> = T extends [infer K, infer V] ? { clave: K; valor: V } : never
type Entrada = ExtraerEntrada<["nombre", string]>
// { clave: "nombre"; valor: string }
Utility types condicionales personalizados
UnpackPromise: desempaquetar promesas y arrays
type Unpack<T> =
T extends Promise<infer U> ? Unpack<U> :
T extends (infer E)[] ? E :
T
type UP1 = Unpack<Promise<string[]>> // string
type UP2 = Unpack<Promise<Promise<number>>> // number
type UP3 = Unpack<boolean[]> // boolean
type UP4 = Unpack<string> // string
ArrayElement: extraer tipo de elemento
type ArrayElement<T> = T extends readonly (infer E)[] ? E : never
type AE1 = ArrayElement<string[]> // string
type AE2 = ArrayElement<readonly number[]> // number
type AE3 = ArrayElement<[string, number]> // string | number
type AE4 = ArrayElement<string> // never
Hacer parcial solo las propiedades de cierto tipo
type OpcionalSi<T, Condicion> = {
[K in keyof T as T[K] extends Condicion ? K : never]?: T[K]
} & {
[K in keyof T as T[K] extends Condicion ? never : K]: T[K]
}
interface Formulario {
nombre: string
email: string
edad: number
activo: boolean
}
type FormStringOpcional = OpcionalSi<Formulario, string>
// { nombre?: string; email?: string } & { edad: number; activo: boolean }
Tipo que extrae las claves con valores de función
type ClavesMetodo<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T]
interface Servicio {
nombre: string
iniciar(): void
detener(): void
estado(): string
versión: number
}
type Métodos = ClavesMetodo<Servicio>
// "iniciar" | "detener" | "estado"
Condicionales recursivos
Los tipos condicionales recursivos operan sobre estructuras anidadas, aplicando transformaciones en todos los niveles de profundidad:
DeepReadonly
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
interface Configuración {
servidor: {
host: string
puerto: number
ssl: { activo: boolean; certificado: string }
}
cache: { ttl: number }
}
type ConfigInmutable = DeepReadonly<Configuración>
// Todas las propiedades en todos los niveles son readonly
const config: ConfigInmutable = {
servidor: {
host: "localhost",
puerto: 3000,
ssl: { activo: true, certificado: "/cert.pem" }
},
cache: { ttl: 3600 }
}
// Error: no se puede asignar a propiedad readonly
// config.servidor.ssl.activo = false
DeepPartial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
type ConfigParcial = DeepPartial<Configuración>
const parcial: ConfigParcial = {
servidor: { puerto: 8080 }
}
Aplanar tipos anidados recursivamente
type DeepFlatten<T> = T extends (infer E)[]
? DeepFlatten<E>
: T
type DF1 = DeepFlatten<number[][][]> // number
type DF2 = DeepFlatten<string[][]> // string
type DF3 = DeepFlatten<boolean> // boolean
Reemplazar tipos en profundidad
type DeepReplace<T, Buscar, Reemplazo> = T extends Buscar
? Reemplazo
: T extends object
? { [K in keyof T]: DeepReplace<T[K], Buscar, Reemplazo> }
: T
interface Datos {
nombre: string
meta: {
creado: Date
modificado: Date
etiquetas: string[]
}
}
type DatosConTimestamp = DeepReplace<Datos, Date, number>
// {
// nombre: string
// meta: {
// creado: number
// modificado: number
// etiquetas: string[]
// }
// }
Patrones avanzados combinados
Tipo seguro para rutas de objetos
type Rutas<T> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? K | `${K}.${Rutas<T[K]> & string}`
: K
: never
}[keyof T]
: never
interface AppState {
usuario: {
nombre: string
perfil: { avatar: string; bio: string }
}
config: { tema: string; idioma: string }
}
type RutasEstado = Rutas<AppState>
// "usuario" | "usuario.nombre" | "usuario.perfil" | "usuario.perfil.avatar"
// | "usuario.perfil.bio" | "config" | "config.tema" | "config.idioma"
Tipo que transforma un objeto en funciones de actualización
type Actualizadores<T> = {
[K in keyof T as `actualizar${Capitalize<string & K>}`]: (
valor: T[K]
) => void
}
interface Estado {
contador: number
mensaje: string
cargando: boolean
}
type AccionesEstado = Actualizadores<Estado>
// {
// actualizarContador: (valor: number) => void
// actualizarMensaje: (valor: string) => void
// actualizarCargando: (valor: boolean) => void
// }
function crearStore<T extends object>(inicial: T): T & Actualizadores<T> {
const estado = { ...inicial } as any
for (const clave in inicial) {
const nombreMetodo = `actualizar${clave.charAt(0).toUpperCase()}${clave.slice(1)}`
estado[nombreMetodo] = (valor: any) => {
estado[clave] = valor
}
}
return estado
}
const store = crearStore({ contador: 0, mensaje: "hola", cargando: false })
store.actualizarContador(5)
store.actualizarMensaje("mundo")
console.log(store.contador) // 5
console.log(store.mensaje) // "mundo"
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en TypeScript
Documentación oficial de TypeScript
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender la sintaxis T extends U ? X : Y de tipos condicionales. Aplicar tipos condicionales distributivos sobre uniones. Usar infer para extraer tipos de funciones, promesas y estructuras. Construir utility types condicionales personalizados con recursividad.