Constraints avanzados en genéricos

Avanzado
TypeScript
TypeScript
Actualizado: 18/04/2026

Constraints avanzados con keyof

Las restricciones genéricas con T extends keyof U permiten que un parámetro de tipo quede limitado a las claves de otro tipo. Este patrón es fundamental para crear funciones que acceden a propiedades de objetos de forma segura y con inferencia completa.

Jerarquia de constraints genéricos

function obtener<T, K extends keyof T>(obj: T, clave: K): T[K] {
    return obj[clave]
}

const servidor = {
    host: "localhost",
    puerto: 3000,
    protocolo: "https" as const
}

const host = obtener(servidor, "host")           // string
const puerto = obtener(servidor, "puerto")       // number
const protocolo = obtener(servidor, "protocolo") // "https"

Este patrón se extiende a escenarios donde un tipo depende de otro para crear relaciones entre parámetros:

function establecer<T, K extends keyof T>(
    obj: T,
    clave: K,
    valor: T[K]
): T {
    return { ...obj, [clave]: valor }
}

const original = { nombre: "Ana", edad: 30, activo: true }
const modificado = establecer(original, "edad", 31)
console.log(modificado)  // { nombre: "Ana", edad: 31, activo: true }

// Error: no se puede asignar string a number
// establecer(original, "edad", "treinta")

Acceso profundo a propiedades

Para acceder a propiedades anidadas de forma tipada, se puede crear un sistema de rutas tipadas:

function obtenerAnidado<
    T,
    K1 extends keyof T,
    K2 extends keyof T[K1]
>(obj: T, clave1: K1, clave2: K2): T[K1][K2] {
    return obj[clave1][clave2]
}

const config = {
    base: { host: "localhost", puerto: 3000 },
    auth: { secreto: "abc123", expiracion: 3600 }
}

const host = obtenerAnidado(config, "base", "host")         // string
const exp = obtenerAnidado(config, "auth", "expiracion")    // number

// Error: "nombre" no existe en config.base
// obtenerAnidado(config, "base", "nombre")

La cadena de restricciones K2 extends keyof T[K1] garantiza que la segunda clave existe como propiedad del objeto al que apunta la primera clave. TypeScript resuelve los tipos de forma progresiva.

Constraints condicionales

Los constraints condicionales combinan restricciones genéricas con tipos condicionales para crear tipos que cambian su comportamiento según el tipo proporcionado:

type IdTipo<T> = T extends { id: infer U } ? U : never

function obtenerIds<T extends { id: any }>(items: T[]): IdTipo<T>[] {
    return items.map(item => item.id)
}

const idsNumericos = obtenerIds([
    { id: 1, nombre: "Ana" },
    { id: 2, nombre: "Luis" }
])
// number[]

const idsTexto = obtenerIds([
    { id: "abc", titulo: "Post 1" },
    { id: "def", titulo: "Post 2" }
])
// string[]

Restricciones que afectan al tipo de retorno

type FormatoSalida<T> = T extends string
    ? string
    : T extends number
    ? number
    : T extends boolean
    ? string
    : never

function formatear<T extends string | number | boolean>(
    valor: T
): FormatoSalida<T> {
    if (typeof valor === "string") {
        return valor.toUpperCase() as FormatoSalida<T>
    }
    if (typeof valor === "number") {
        return (valor * 100) as FormatoSalida<T>
    }
    return (valor ? "si" : "no") as FormatoSalida<T>
}

const texto = formatear("hola")     // string
const número = formatear(0.5)       // number
const booleano = formatear(true)    // string

console.log(texto)     // "HOLA"
console.log(número)    // 50
console.log(booleano)  // "si"

Constraints con tipos mapeados

type SoloPropiedadesDeTipo<T, TipoValor> = {
    [K in keyof T as T[K] extends TipoValor ? K : never]: T[K]
}

interface Empleado {
    nombre: string
    edad: number
    departamento: string
    salario: number
    activo: boolean
}

type PropiedadesTexto = SoloPropiedadesDeTipo<Empleado, string>
// { nombre: string; departamento: string }

type PropiedadesNumericas = SoloPropiedadesDeTipo<Empleado, number>
// { edad: number; salario: number }

function obtenerValoresNumericos<T>(
    obj: T,
    claves: (keyof SoloPropiedadesDeTipo<T, number>)[]
): number[] {
    return claves.map(clave => (obj as any)[clave])
}

const emp: Empleado = {
    nombre: "Carlos",
    edad: 35,
    departamento: "IT",
    salario: 45000,
    activo: true
}

const nums = obtenerValoresNumericos(emp, ["edad", "salario"])
console.log(nums)  // [35, 45000]

Genéricos recursivos

Los genéricos recursivos definen tipos que se refieren a si mismos, permitiendo modelar estructuras de datos anidadas como arboles, listas enlazadas y objetos profundos:

type Arbol<T> = {
    valor: T
    hijos: Arbol<T>[]
}

function crearNodo<T>(valor: T, ...hijos: Arbol<T>[]): Arbol<T> {
    return { valor, hijos }
}

const árbol = crearNodo("raiz",
    crearNodo("hijo1",
        crearNodo("nieto1"),
        crearNodo("nieto2")
    ),
    crearNodo("hijo2",
        crearNodo("nieto3")
    )
)

function recorrer<T>(nodo: Arbol<T>, nivel: number = 0): void {
    console.log(" ".repeat(nivel * 2) + nodo.valor)
    for (const hijo of nodo.hijos) {
        recorrer(hijo, nivel + 1)
    }
}

recorrer(árbol)
// raiz
//   hijo1
//     nieto1
//     nieto2
//   hijo2
//     nieto3

DeepReadonly recursivo

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; maxSize: number }
}

const config: DeepReadonly<Configuración> = {
    servidor: {
        host: "localhost",
        puerto: 3000,
        ssl: { activo: true, certificado: "/ruta/cert.pem" }
    },
    cache: { ttl: 3600, maxSize: 1000 }
}

// Error: no se puede asignar a propiedad readonly
// config.servidor.ssl.activo = false

DeepPartial recursivo

type DeepPartial<T> = T extends object
    ? { [K in keyof T]?: DeepPartial<T[K]> }
    : T

function fusionarProfundo<T extends object>(
    base: T,
    parcial: DeepPartial<T>
): T {
    const resultado = { ...base }
    for (const clave in parcial) {
        const valor = parcial[clave]
        if (valor !== undefined && typeof valor === "object" && !Array.isArray(valor)) {
            (resultado as any)[clave] = fusionarProfundo(
                (base as any)[clave],
                valor as any
            )
        } else if (valor !== undefined) {
            (resultado as any)[clave] = valor
        }
    }
    return resultado
}

const configBase: Configuración = {
    servidor: {
        host: "localhost",
        puerto: 3000,
        ssl: { activo: false, certificado: "" }
    },
    cache: { ttl: 3600, maxSize: 1000 }
}

const resultado = fusionarProfundo(configBase, {
    servidor: { puerto: 8080, ssl: { activo: true } }
})

console.log(resultado.servidor.puerto)       // 8080
console.log(resultado.servidor.ssl.activo)   // true
console.log(resultado.servidor.host)         // "localhost" (preservado)

Tuplas variádicas

Las tuplas variádicas permiten que los genéricos operen sobre tuplas de longitud variable, capturando y transformando cada posición de forma tipada:

type Concatenar<T extends unknown[], U extends unknown[]> = [...T, ...U]

type A = Concatenar<[string, number], [boolean]>
// [string, number, boolean]

type B = Concatenar<[1, 2], [3, 4, 5]>
// [1, 2, 3, 4, 5]
function concatenar<T extends unknown[], U extends unknown[]>(
    a: [...T],
    b: [...U]
): [...T, ...U] {
    return [...a, ...b]
}

const resultado = concatenar([1, "dos"] as [number, string], [true] as [boolean])
// [number, string, boolean]
console.log(resultado)  // [1, "dos", true]

Funciones con argumentos variádicos

function aplicar<T extends unknown[], R>(
    fn: (...args: T) => R,
    args: T
): R {
    return fn(...args)
}

function sumar(a: number, b: number, c: number): number {
    return a + b + c
}

const total = aplicar(sumar, [10, 20, 30])
console.log(total)  // 60

Transformar tipos de tupla

type Envolver<T extends unknown[]> = {
    [K in keyof T]: { valor: T[K] }
}

type Original = [string, number, boolean]
type Envuelta = Envolver<Original>
// [{ valor: string }, { valor: number }, { valor: boolean }]

function envolver<T extends unknown[]>(...args: T): Envolver<T> {
    return args.map(valor => ({ valor })) as Envolver<T>
}

const resultado = envolver("hola", 42, true)
console.log(resultado)  // [{ valor: "hola" }, { valor: 42 }, { valor: true }]

Factorías genéricas

Las factorías genéricas crean instancias de clases de forma tipada, usando la firma del constructor como restricción genérica:

function crear<T>(Constructor: new () => T): T {
    return new Constructor()
}

class Conexion {
    estado = "conectado"
}

class Logger {
    nivel = "info"
}

const conn = crear(Conexion)      // Conexion
console.log(conn.estado)          // "conectado"

const logger = crear(Logger)      // Logger
console.log(logger.nivel)         // "info"

Factorías con argumentos

function crearCon<T, A extends unknown[]>(
    Constructor: new (...args: A) => T,
    ...args: A
): T {
    return new Constructor(...args)
}

class Servicio {
    constructor(
        public nombre: string,
        public puerto: number
    ) {}
}

const servicio = crearCon(Servicio, "api", 8080)
console.log(servicio.nombre)   // "api"
console.log(servicio.puerto)   // 8080

Registro de factorías

class RegistroFactorias {
    private fabricas = new Map<string, new (...args: any[]) => any>()

    registrar<T>(nombre: string, Constructor: new (...args: any[]) => T): void {
        this.fabricas.set(nombre, Constructor)
    }

    crear<T>(nombre: string, ...args: any[]): T {
        const Constructor = this.fabricas.get(nombre)
        if (!Constructor) {
            throw new Error(`Fabrica "${nombre}" no registrada`)
        }
        return new Constructor(...args) as T
    }
}

class ConexionDB {
    constructor(public url: string) {}
}

class ConexionCache {
    constructor(public host: string, public puerto: number) {}
}

const registro = new RegistroFactorias()
registro.registrar("db", ConexionDB)
registro.registrar("cache", ConexionCache)

const db = registro.crear<ConexionDB>("db", "postgres://localhost:5432")
const cache = registro.crear<ConexionCache>("cache", "localhost", 6379)

console.log(db.url)         // "postgres://localhost:5432"
console.log(cache.puerto)   // 6379

Inferencia avanzada con genéricos

TypeScript puede inferir tipos complejos a partir del contexto en que se usan los genéricos. Este mecanismo permite que funciones y clases deduzcan tipos sin necesidad de anotaciones explícitas.

Inferencia desde callbacks

function procesarConResultado<T, R>(
    datos: T[],
    procesador: (item: T, indice: number) => R
): R[] {
    return datos.map((item, indice) => procesador(item, indice))
}

const usuarios = [
    { nombre: "Ana", puntos: 100 },
    { nombre: "Luis", puntos: 200 }
]

// R se infiere como string
const resumen = procesarConResultado(
    usuarios,
    (u, i) => `${i + 1}. ${u.nombre}: ${u.puntos} pts`
)
console.log(resumen)  // ["1. Ana: 100 pts", "2. Luis: 200 pts"]

Inferencia desde valores literales

function crearAccion<T extends string, P>(tipo: T, payload: P): { tipo: T; payload: P } {
    return { tipo, payload }
}

const accion = crearAccion("AGREGAR_USUARIO", { nombre: "Ana", edad: 30 })
// tipo: { tipo: "AGREGAR_USUARIO"; payload: { nombre: string; edad: number } }

console.log(accion.tipo)              // "AGREGAR_USUARIO"
console.log(accion.payload.nombre)    // "Ana"

Inferencia en cadenas de métodos

class Pipeline<T> {
    constructor(private valor: T) {}

    pipe<U>(fn: (valor: T) => U): Pipeline<U> {
        return new Pipeline(fn(this.valor))
    }

    resultado(): T {
        return this.valor
    }
}

const valor = new Pipeline("  Hola Mundo  ")
    .pipe(s => s.trim())
    .pipe(s => s.toLowerCase())
    .pipe(s => s.split(" "))
    .pipe(arr => arr.length)
    .resultado()

console.log(valor)  // 2

Cada llamada a pipe infiere automáticamente el nuevo tipo U a partir del valor de retorno de la función proporcionada. La cadena transforma string a string[] a number sin ninguna anotación explícita.

Emisor de eventos tipado

Un emisor de eventos tipado garantiza que los tipos de los datos emitidos coincidan con los declarados en el mapa de eventos:

type MapaEventos = {
    conexión: { host: string; puerto: number }
    mensaje: { contenido: string; autor: string }
    error: { código: number; descripcion: string }
    desconexion: void
}

class EmisorTipado<T extends Record<string, any>> {
    private listeners = new Map<keyof T, Set<(datos: any) => void>>()

    on<K extends keyof T>(evento: K, callback: (datos: T[K]) => void): void {
        if (!this.listeners.has(evento)) {
            this.listeners.set(evento, new Set())
        }
        this.listeners.get(evento)!.add(callback)
    }

    off<K extends keyof T>(evento: K, callback: (datos: T[K]) => void): void {
        this.listeners.get(evento)?.delete(callback)
    }

    emit<K extends keyof T>(evento: K, datos: T[K]): void {
        this.listeners.get(evento)?.forEach(cb => cb(datos))
    }
}

const emisor = new EmisorTipado<MapaEventos>()

emisor.on("conexión", datos => {
    console.log(`Conectado a ${datos.host}:${datos.puerto}`)
})

emisor.on("mensaje", datos => {
    console.log(`${datos.autor}: ${datos.contenido}`)
})

emisor.on("error", datos => {
    console.log(`Error ${datos.código}: ${datos.descripcion}`)
})

emisor.emit("conexión", { host: "localhost", puerto: 3000 })
emisor.emit("mensaje", { contenido: "Hola", autor: "Ana" })

// Error: propiedad "texto" no existe en tipo { contenido: string; autor: string }
// emisor.emit("mensaje", { texto: "fallo" })

Cliente API tipado

Un cliente API tipado utiliza genéricos para garantizar que las rutas, los cuerpos de petición y las respuestas sean coherentes:

interface Rutas {
    "/usuarios": {
        GET: { respuesta: { id: number; nombre: string }[] }
        POST: { cuerpo: { nombre: string; email: string }; respuesta: { id: number } }
    }
    "/usuarios/:id": {
        GET: { respuesta: { id: number; nombre: string; email: string } }
        PUT: { cuerpo: { nombre?: string; email?: string }; respuesta: { ok: boolean } }
        DELETE: { respuesta: { ok: boolean } }
    }
}

type MetodoHTTP = "GET" | "POST" | "PUT" | "DELETE"

type ObtenerRespuesta<
    R extends keyof Rutas,
    M extends keyof Rutas[R]
> = Rutas[R][M] extends { respuesta: infer Resp } ? Resp : never

type ObtenerCuerpo<
    R extends keyof Rutas,
    M extends keyof Rutas[R]
> = Rutas[R][M] extends { cuerpo: infer Body } ? Body : never

class ClienteAPI {
    async get<R extends keyof Rutas>(
        ruta: R
    ): Promise<ObtenerRespuesta<R, "GET">> {
        const res = await fetch(ruta as string)
        return res.json()
    }

    async post<R extends keyof Rutas>(
        ruta: R,
        cuerpo: ObtenerCuerpo<R, "POST">
    ): Promise<ObtenerRespuesta<R, "POST">> {
        const res = await fetch(ruta as string, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(cuerpo)
        })
        return res.json()
    }
}

const api = new ClienteAPI()

// TypeScript infiere el tipo de respuesta automáticamente
async function ejemplo() {
    const usuarios = await api.get("/usuarios")
    // { id: number; nombre: string }[]
    console.log(usuarios[0].nombre)

    const nuevo = await api.post("/usuarios", {
        nombre: "Elena",
        email: "elena@ejemplo.com"
    })
    // { id: number }
    console.log(nuevo.id)
}

El cliente API utiliza una jerarquía de tipos indexados donde cada ruta define sus métodos HTTP permitidos, los cuerpos requeridos y los tipos de respuesta. Los tipos condicionales con infer extraen automáticamente el tipo correcto en cada operación.

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

Aplicar constraints avanzados con T extends keyof U y tipos condicionales. Implementar genéricos recursivos y tuplas variádicas. Crear factorías genéricas y patrones de inferencia con genéricos. Diseñar emisores de eventos y clientes API tipados.