Tipos condicionales e infer

Avanzado
TypeScript
TypeScript
Actualizado: 17/04/2026

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.

Flujo de tipos condicionales e infer

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 extends en tipos condicionales no significa herencia de clases. Significa asignabilidad: si un valor de tipo T se puede asignar a una variable de tipo U, entonces T extends U es 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 del extends entre 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 - Autor del tutorial

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.