Hasta ahora has escrito interfaces y types a mano, campo por campo. Eso funciona, pero tiene un coste: si el tipo base cambia, tienes que actualizar cada derivado a mano. Los utility types resuelven eso: son tipos genéricos que TypeScript incluye de serie y que transforman o derivan tipos a partir de otros.
¿Qué es un utility type?#
Un utility type es un tipo genérico de la librería estándar de TypeScript. Lo aplicas sobre un tipo que ya tienes —una interface, un type alias— y obtienes uno nuevo, transformado de alguna forma concreta: con todos los campos opcionales, con solo algunos, sin algunos, de solo lectura, etc.
// Sin utility types: tienes que escribir una segunda forma a mano.
// Si Heroe cambia, ActualizacionHeroe queda desincronizado.
interface ActualizacionHeroeMal {
nombre?: string;
rol?: string;
partidas?: number;
victorias?: number;
}
// Con utility types: se deriva de Heroe, se actualiza solo.
// Una sola fuente de verdad.
type ActualizacionHeroeBien = Partial<Heroe>;La clave es la única fuente de verdad: defines Heroe una vez y el resto
de tipos se derivan de ella. Si mañana añades un campo a Heroe, todos los
utility types que la usan lo recogen solos.
Partial<T>#
Partial<T> convierte todos los campos de T en opcionales. Es el utility
correcto cuando solo quieres actualizar una parte de un registro: en vez de
exigir los cuatro campos, la función acepta cualquier subconjunto.
// Partial<Heroe>: todos los campos con "?".
// Equivale a escribir { nombre?: string; rol?: string; ... } a mano,
// pero derivado de Heroe: si Heroe cambia, Partial lo refleja solo.
function actualizarHeroe(base: Heroe, cambios: Partial<Heroe>): Heroe {
return { ...base, ...cambios };
}
// Solo mandas los campos que cambian.
const tracerActualizado = actualizarHeroe(tracer, { partidas: 125 });Required<T>#
Required<T> es el inverso de Partial: convierte todos los campos en
obligatorios, aunque el tipo original los tuviera como opcionales. Útil cuando
quieres modelar un estado “borrador” (campos opcionales) y un estado “enviado”
(todos obligatorios).
// BorradorHeroe: todos opcionales mientras se rellena.
interface BorradorHeroe {
nombre?: string;
rol?: string;
partidas?: number;
victorias?: number;
}
// Required<BorradorHeroe>: al enviar, todos obligatorios.
type HeroeCompleto = Required<BorradorHeroe>;Pick<T, K>#
Pick<T, K> construye un tipo nuevo con solo los campos K de T. Lo usas
cuando una vista o función solo necesita una parte del tipo completo: no copias
la forma a mano, la derivas de la fuente original.
// Solo necesitas nombre y rol para una tarjeta de vista rápida.
// Pick<Heroe, "nombre" | "rol"> garantiza que el tipo derivado
// solo tiene esos dos campos: ni más ni menos.
function tarjetaHeroe(h: Heroe): Pick<Heroe, "nombre" | "rol"> {
return { nombre: h.nombre, rol: h.rol };
}Omit<T, K>#
Omit<T, K> es el reverso de Pick: construye un tipo con todos los campos
de T excepto los de K. En vez de elegir qué conservar, eliges qué quitar.
// Al registrar un héroe nuevo, aún no tiene estadísticas.
// Omit<Heroe, "partidas" | "victorias"> deja solo nombre y rol.
type FichaPublica = Omit<Heroe, "partidas" | "victorias">;
function registrarHeroe(ficha: FichaPublica): void {
console.log("Registrado: " + ficha.nombre + " (" + ficha.rol + ")");
}La diferencia entre Pick y Omit es de intención. Con cuatro campos, ambos
pueden dar el mismo tipo resultante. Pero si Heroe ganara cinco campos nuevos,
Pick<Heroe, "nombre" | "rol"> los ignoraría todos, mientras que
Omit<Heroe, "partidas" | "victorias"> los incluiría todos.
Record<K, V>#
Record<K, V> describe un objeto-diccionario: todas sus claves son del tipo
K y todos sus valores son del tipo V. Es la alternativa legible al patrón
{ [clave: string]: Valor }.
// Un objeto cuyas claves son strings (el rol) y cuyos valores son listas de heroes.
function agruparPorRol(lista: Heroe[]): Record<string, Heroe[]> {
// Acumulador vacío: TypeScript sabe que sus claves son strings y sus valores Heroe[].
const grupos: Record<string, Heroe[]> = {};
// Recorremos cada heroe de la lista.
for (const h of lista) {
// Si la clave aún no existe, la inicializamos con un array vacío.
if (!grupos[h.rol]) { grupos[h.rol] = []; }
// Añadimos el heroe al grupo correspondiente a su rol.
grupos[h.rol].push(h);
}
return grupos;
}Cuando las claves son conocidas de antemano, puedes acotarlas con un union
literal: Record<"Daño" | "Tanque" | "Apoyo", Heroe[]>. Así TypeScript avisa
si intentas acceder con una clave que no existe.
Readonly<T>#
Readonly<T> marca todos los campos del tipo como de solo lectura. Una vez
creado el objeto, TypeScript impide cualquier reasignación. A diferencia del
modificador readonly campo a campo en la interface —que afecta a todos los
usos del tipo—, Readonly<T> lo aplicas en un punto concreto: el mismo tipo
sigue siendo mutable en otros sitios.
// tracer no debería modificarse: Readonly<Heroe> lo garantiza.
const tracer: Readonly<Heroe> = {
nombre: "Tracer",
rol: "Daño",
partidas: 120,
victorias: 78,
};
// TypeScript impide la reasignación antes de ejecutar.
// error: no se puede asignar a una propiedad readonly
// tracer.partidas = 999;ReturnType<typeof fn>#
Antes de ver el utility, una aclaración importante: el typeof que ves aquí
no es el mismo typeof que usaste en narrowing. En narrowing, typeof se
ejecuta en tiempo de ejecución y devuelve un string como "string" o "number".
Aquí, dentro de un tipo (ReturnType<typeof fn>), typeof es un operador de tipo:
le dice a TypeScript “dame el tipo estático de esta función o valor”, y eso
ocurre en tiempo de compilación, no en tiempo de ejecución. Es la forma de
pasarle una función —no su resultado— a un utility type.
ReturnType<typeof fn> extrae el tipo de retorno de una función. No la llama:
actúa sobre el tipo de la función en tiempo de compilación. Es útil cuando
el tipo de retorno es complejo o cuando quieres que el tipo derivado se
actualice solo si la función cambia.
function calcularResumen(lista: Heroe[]) {
return { cantidad: lista.length, totalPartidas: 0, winrate: 0 };
}
// ResumenEquipo se deriva del retorno de calcularResumen.
// Si mañana añades un campo al objeto, ResumenEquipo lo recoge sin tocar nada.
type ResumenEquipo = ReturnType<typeof calcularResumen>;El patrón ReturnType<typeof fn> cierra el círculo de la única fuente de
verdad: los tipos se derivan de la lógica real, no al revés.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué hace `Partial<Heroe>`?
Tu turno#
Tienes tres funciones con tipos incorrectos o demasiado amplios. Tu tarea:
reemplaza cada tipo por el utility type correcto derivado de Heroe, sin
duplicar la forma del tipo a mano. El panel “Problemas” y los TODO te guían.
Cuando el panel quede vacío y al pulsar “Ejecutar” veas los resultados en la
consola, has terminado.
Ejercicio · en esta página
Tipos derivados para el Team Builder
Tienes tres funciones con tipos incorrectos o demasiado amplios. Tu tarea: reemplaza cada tipo por el utility type correcto derivado de Heroe. El panel "Problemas" y los TODO te guían.
Paso 1: Que funcione
- El panel "Problemas" queda vacío: no hay errores de tipo.
- actualizarHeroe acepta `Partial<Heroe>` como segundo argumento.
- tarjetaHeroe devuelve `Pick<Heroe, "nombre" | "rol">`.
- agruparPorRol devuelve `Record<string, Heroe[]>`.
Paso 2: Que sea preciso
- La clave de Record no es `string` genérico sino un tipo literal con los roles reales: `Record<Rol, Heroe[]>`.
- TypeScript detecta en el editor si intentas usar una clave de rol que no existe.
- Sin `any` en ningún sitio.
Paso 3: Que todo derive de Heroe
- Defines alias de tipo con utility types: `TarjetaHeroe`, `ActualizacionHeroe` (Partial + Omit), `GrupoPorRol`.
- Si cambias Heroe, todos los tipos derivados se ajustan solos: cero definiciones duplicadas.
- Usas `ReturnType<typeof actualizarHeroe>` para tipar el resultado. Sin `any`, sin `@ts-ignore`.
Ver soluciones
// SOLUCIÓN OK — Los utility types sustituyen a "any": el panel queda vacío.
// El punto débil: el tipo de retorno de agruparPorRol usa string como clave,
// que es demasiado amplio. Funciona, pero hay un utility más preciso.
interface Heroe {
readonly nombre: string;
readonly rol: string;
partidas: number;
victorias: number;
}
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
{ nombre: "Ana", rol: "Apoyo", partidas: 140, victorias: 98 },
];
// Partial<Heroe>: todos los campos de Heroe pasan a ser opcionales.
// Perfecto para una actualización parcial: solo mandas los campos que cambian.
function actualizarHeroe(base: Heroe, cambios: Partial<Heroe>): Heroe {
return { ...base, ...cambios };
}
// Pick<Heroe, "nombre" | "rol">: solo estos dos campos del tipo original.
// La función promete devolver exactamente eso, ni más ni menos.
function tarjetaHeroe(h: Heroe): Pick<Heroe, "nombre" | "rol"> {
return { nombre: h.nombre, rol: h.rol };
}
// Record<string, Heroe[]>: un objeto con claves string y valores Heroe[].
// Clave amplia (string): válida, aunque no acota cuáles son los roles posibles.
function agruparPorRol(lista: Heroe[]): Record<string, Heroe[]> {
const grupos: Record<string, Heroe[]> = {};
for (const h of lista) {
if (!grupos[h.rol]) {
grupos[h.rol] = [];
}
grupos[h.rol].push(h);
}
return grupos;
}
const tracer = equipo[0];
const tracerActualizado = actualizarHeroe(tracer, { partidas: 125 });
console.log("Actualizado: " + tracerActualizado.nombre + " — " + tracerActualizado.partidas + " partidas");
const tarjeta = tarjetaHeroe(tracer);
console.log("Tarjeta: " + tarjeta.nombre + " (" + tarjeta.rol + ")");
const porRol = agruparPorRol(equipo);
const roles = Object.keys(porRol);
for (const rol of roles) {
const nombres = porRol[rol].map(function(h) { return h.nombre; }).join(", ");
console.log(rol + ": " + nombres);
} Por qué este nivel
- Sustituyes los tres `any` por `Partial<Heroe>`, `Pick<Heroe, "nombre" | "rol">` y `Record<string, Heroe[]>`. El panel queda vacío.
- La clave de Record es `string`: válida, pero no acota los roles posibles. TypeScript no puede avisar si usas una clave que no existe.
- Es la solución directa de los TODO: correcta, pero hay margen para ser más preciso.
// SOLUCIÓN MEJOR — Utility type correcto en cada sitio: Pick, Partial, Record con clave literal.
// Mejora respecto a la ok: la clave de Record no es string genérico,
// sino un tipo literal con los roles reales. TypeScript verifica que solo
// usas claves válidas.
interface Heroe {
readonly nombre: string;
readonly rol: string;
partidas: number;
victorias: number;
}
// Tipo literal que acota los roles posibles a los tres que existen.
// Si mañana añades "Soporte", basta con añadirlo aquí: TypeScript te avisa
// en todos los sitios que lo usen.
type Rol = "Daño" | "Tanque" | "Apoyo";
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
{ nombre: "Ana", rol: "Apoyo", partidas: 140, victorias: 98 },
];
// Partial<Heroe>: todos los campos opcionales para la actualización parcial.
function actualizarHeroe(base: Heroe, cambios: Partial<Heroe>): Heroe {
return { ...base, ...cambios };
}
// Pick<Heroe, "nombre" | "rol">: tipo derivado con exactamente esos dos campos.
function tarjetaHeroe(h: Heroe): Pick<Heroe, "nombre" | "rol"> {
return { nombre: h.nombre, rol: h.rol };
}
// Record<Rol, Heroe[]>: las claves son los roles conocidos, no cualquier string.
// Si intentas acceder con una clave que no sea "Daño" | "Tanque" | "Apoyo",
// TypeScript lo detecta en el editor antes de ejecutar.
function agruparPorRol(lista: Heroe[]): Record<Rol, Heroe[]> {
// Partimos de un objeto con los tres roles vacíos para que TypeScript
// esté seguro de que el objeto cumple Record<Rol, Heroe[]> desde el inicio.
const grupos: Record<Rol, Heroe[]> = { Daño: [], Tanque: [], Apoyo: [] };
for (const h of lista) {
// "as Rol" es necesario porque la interface declara rol como string, no como Rol.
// Sabemos que los datos del array solo contienen los tres roles válidos,
// pero TypeScript no puede inferirlo desde la interface. En la solución excelente,
// esto se resuelve tipando rol: Rol directamente en la interface.
grupos[h.rol as Rol].push(h);
}
return grupos;
}
const tracer = equipo[0];
const tracerActualizado = actualizarHeroe(tracer, { partidas: 125 });
console.log(
"Actualizado: " +
tracerActualizado.nombre +
" — " +
tracerActualizado.partidas +
" partidas",
);
const tarjeta = tarjetaHeroe(tracer);
console.log("Tarjeta: " + tarjeta.nombre + " (" + tarjeta.rol + ")");
const porRol = agruparPorRol(equipo);
const roles: Rol[] = ["Daño", "Tanque", "Apoyo"];
for (const rol of roles) {
const nombres = porRol[rol]
.map(function (h) {
return h.nombre;
})
.join(", ");
console.log(rol + ": " + nombres);
} Por qué es mejor que el anterior
- Introduces `type Rol` con los roles reales y usas `Record<Rol, Heroe[]>`: las claves ya no son cualquier string.
- TypeScript detecta en el editor si intentas acceder con una clave inválida: la red de seguridad cubre los roles.
- El código es igual de conciso pero más preciso: el tipo dice exactamente qué claves son válidas.
// SOLUCIÓN EXCELENTE — TODOS los tipos derivados de la única fuente: Heroe.
// Si cambias Heroe, todos los tipos derivados se ajustan solos.
// Cero types escritos a mano. Cero any. Cero duplicación.
// Rol se declara primero para poder usarlo en la interface Heroe.
// Así Heroe["rol"] devuelve exactamente este union literal, no string genérico.
type Rol = "Daño" | "Tanque" | "Apoyo";
// Al tipar "rol: Rol" en la interface, Heroe["rol"] === Rol.
// Todos los tipos derivados que lean Heroe["rol"] obtienen el union literal, no string.
interface Heroe {
readonly nombre: string;
readonly rol: Rol;
partidas: number;
victorias: number;
}
// TarjetaHeroe se deriva de Heroe con Pick: si renombras "nombre" en Heroe,
// el error aparece aquí automáticamente. No hay una segunda definición de la forma.
type TarjetaHeroe = Pick<Heroe, "nombre" | "rol">;
// ActualizacionHeroe se deriva de Heroe con Partial: todos los campos opcionales.
// Y Omit quita "nombre" y "rol" porque son readonly y no deben actualizarse nunca.
// El resultado: solo partidas y victorias son actualizables, y ambas opcionales.
type ActualizacionHeroe = Partial<Omit<Heroe, "nombre" | "rol">>;
// GrupoPorRol se deriva de Heroe y Rol con Record: las claves son los roles reales,
// los valores son listas del tipo completo Heroe. Una sola línea, sin repetir la forma.
type GrupoPorRol = Record<Rol, Heroe[]>;
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
{ nombre: "Ana", rol: "Apoyo", partidas: 140, victorias: 98 },
];
// ActualizacionHeroe: TypeScript impide pasar nombre o rol (son omitidos).
// Descomenta la línea del TODO para ver el error antes de ejecutar:
// actualizarHeroe(equipo[0], { nombre: "NuevoNombre" }); // error: nombre no es actualizable
function actualizarHeroe(base: Heroe, cambios: ActualizacionHeroe): Heroe {
return { ...base, ...cambios };
}
// TarjetaHeroe: la firma dice exactamente qué devuelve, derivado de Heroe.
function tarjetaHeroe(h: Heroe): TarjetaHeroe {
return { nombre: h.nombre, rol: h.rol };
}
// GrupoPorRol: las claves son los roles conocidos, no cualquier string.
// Como h.rol ya es Rol (no string), no hace falta ningún cast.
function agruparPorRol(lista: Heroe[]): GrupoPorRol {
// Partimos de un objeto con los tres roles vacíos: TypeScript verifica que cumple GrupoPorRol.
const grupos: GrupoPorRol = { Daño: [], Tanque: [], Apoyo: [] };
// h.rol es Rol, así que se puede usar como clave de GrupoPorRol directamente.
for (const h of lista) {
grupos[h.rol].push(h);
}
return grupos;
}
// ReturnType extrae el tipo de retorno de una función ya definida.
// Si cambias lo que devuelve actualizarHeroe, ResultadoActualizar se actualiza solo.
type ResultadoActualizar = ReturnType<typeof actualizarHeroe>;
const tracer = equipo[0];
// Solo se pueden actualizar partidas o victorias: nombre y rol están omitidos.
const tracerActualizado: ResultadoActualizar = actualizarHeroe(tracer, {
partidas: 125,
});
console.log(
"Actualizado: " +
tracerActualizado.nombre +
" — " +
tracerActualizado.partidas +
" partidas",
);
const tarjeta: TarjetaHeroe = tarjetaHeroe(tracer);
console.log("Tarjeta: " + tarjeta.nombre + " (" + tarjeta.rol + ")");
const porRol: GrupoPorRol = agruparPorRol(equipo);
const roles: Rol[] = ["Daño", "Tanque", "Apoyo"];
for (const rol of roles) {
const nombres = porRol[rol]
.map(function (h) {
return h.nombre;
})
.join(", ");
console.log(rol + ": " + nombres);
} Por qué es mejor que el anterior
- Todos los tipos son utility types sobre Heroe: `TarjetaHeroe`, `ActualizacionHeroe` (Partial + Omit), `GrupoPorRol`. Si cambias Heroe, todo se ajusta solo.
- `ActualizacionHeroe` combina Partial y Omit: solo se pueden actualizar partidas y victorias, nunca nombre ni rol (que son datos de origen).
- ReturnType<typeof actualizarHeroe> cierra el círculo: el tipo de la variable resultado se deriva de la función, no de una anotación manual.