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.

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
pipeinfiere automáticamente el nuevo tipoUa partir del valor de retorno de la función proporcionada. La cadena transformastringastring[]anumbersin 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
inferextraen automáticamente el tipo correcto en cada operación.
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.