Tema
Utility types
Imagina que tienes un tipo Usuario y necesitas: la versión para crear (sin id), la versión para actualizar (todo opcional) y la versión pública (sin password). Podrías escribir tres interfaces casi iguales... o derivarlas del original con los utility types que TypeScript trae integrados. Si cambias Usuario, todo lo demás se actualiza solo.
Concepto
Los utility types son funciones de tipos: reciben un tipo y devuelven otro transformado. Son como los filtros de una app de fotos: la foto original no cambia, pero obtienes versiones nuevas a partir de ella.
Todos están construidos con genéricos (ver Genéricos), así que se usan con la sintaxis Utilidad<Tipo>.
Usaremos este tipo base en toda la lección:
ts
interface Usuario {
id: number;
nombre: string;
email: string;
password: string;
rol: 'admin' | 'cliente';
telefono?: string;
}Partial y Required
Partial<T> vuelve opcionales todas las propiedades. Required<T> hace lo contrario: las vuelve obligatorias.
ts
// Para actualizar: el cliente puede mandar solo algunos campos
type ActualizarUsuario = Partial<Usuario>;
// { id?: number; nombre?: string; email?: string; ... }
function actualizarUsuario(id: number, cambios: Partial<Usuario>): void {
console.log(`Actualizando ${id}`, cambios);
}
actualizarUsuario(7, { telefono: '33 1234 5678' }); // ✅ solo un campo
// Required: telefono deja de ser opcional
type UsuarioCompleto = Required<Usuario>;
const u: UsuarioCompleto = {
id: 1, nombre: 'Ana', email: 'a@a.mx', password: 'x', rol: 'cliente'
};
// ❌ Property 'telefono' is missingReadonly
Readonly<T> marca todas las propiedades como de solo lectura:
ts
const CONFIG: Readonly<{ apiUrl: string; reintentos: number }> = {
apiUrl: 'https://api.mitienda.mx',
reintentos: 3
};
CONFIG.reintentos = 5;
// ❌ Cannot assign to 'reintentos' because it is a read-only property.
// Muy útil para estados que no deben mutarse (React, Redux)
function mostrar(usuario: Readonly<Usuario>) {
// usuario.nombre = 'otro'; ❌
return usuario.nombre;
}Pick y Omit
Pick<T, Claves> toma solo algunas propiedades. Omit<T, Claves> quita algunas. Las claves se pasan como unión de literales.
ts
// Lo que se muestra en una tarjeta de perfil
type TarjetaUsuario = Pick<Usuario, 'nombre' | 'email'>;
// { nombre: string; email: string }
// Lo que se puede enviar al frontend: todo menos el password
type UsuarioPublico = Omit<Usuario, 'password'>;
// Para crear: el id lo genera la base de datos
type CrearUsuario = Omit<Usuario, 'id'>;
function sanitizar(u: Usuario): UsuarioPublico {
const { password, ...resto } = u; // quitamos el password con rest
return resto;
}Buena práctica
Usa Pick cuando necesites pocas propiedades y Omit cuando necesites casi todas. Así el tipo derivado sigue leyéndose fácil.
Record
Ya lo conoces de Interfaces y types: Record<K, V> crea un objeto con claves K y valores V.
ts
type Rol = Usuario['rol']; // 'admin' | 'cliente' (acceso indexado)
const permisos: Record<Rol, string[]> = {
admin: ['leer', 'escribir', 'borrar'],
cliente: ['leer']
};
// Índice de usuarios por id
const porId: Record<number, Usuario> = {};Exclude, Extract y NonNullable
Estos trabajan sobre uniones:
Exclude<Union, Quitar>: quita miembros de la unión.Extract<Union, Quedarse>: se queda solo con los miembros indicados.NonNullable<T>: quitanullyundefined.
ts
type Estado = 'pendiente' | 'pagado' | 'enviado' | 'entregado' | 'cancelado';
type EstadoActivo = Exclude<Estado, 'cancelado' | 'entregado'>;
// 'pendiente' | 'pagado' | 'enviado'
type EstadoFinal = Extract<Estado, 'entregado' | 'cancelado'>;
// 'entregado' | 'cancelado'
type QuizaTexto = string | null | undefined;
type Texto = NonNullable<QuizaTexto>; // string
// Caso real: filtrar nulls de un array con tipo correcto
const correos: (string | null)[] = ['a@a.mx', null, 'b@b.mx'];
const validos = correos.filter((c): c is NonNullable<typeof c> => c !== null);
// validos: string[]ReturnType, Parameters y Awaited
Estos extraen tipos de funciones. Muy útiles cuando una librería no exporta los tipos que necesitas.
ts
function crearSesion(usuario: Usuario, recordar: boolean) {
return {
token: crypto.randomUUID(),
usuarioId: usuario.id,
expira: recordar ? Date.now() + 30 * 86400000 : Date.now() + 3600000
};
}
// El tipo de lo que devuelve la función (sin escribirlo a mano)
type Sesion = ReturnType<typeof crearSesion>;
// { token: string; usuarioId: number; expira: number }
// Una tupla con los tipos de los parámetros
type ParamsSesion = Parameters<typeof crearSesion>;
// [usuario: Usuario, recordar: boolean]
// Awaited "desenvuelve" una Promise
async function obtenerPedidos() {
return [{ id: 'PED-1', total: 790 }];
}
type RetornoCrudo = ReturnType<typeof obtenerPedidos>;
// Promise<{ id: string; total: number }[]>
type Pedidos = Awaited<ReturnType<typeof obtenerPedidos>>;
// { id: string; total: number }[]Fíjate en typeof crearSesion: los utility types trabajan con tipos, así que usamos typeof para obtener el tipo de una función (que es un valor).
Error común
Escribir ReturnType<crearSesion> sin typeof. Da error porque crearSesion es un valor, no un tipo: 'crearSesion' refers to a value, but is being used as a type here.
Caso real: DTOs de una API
Un DTO (Data Transfer Object) es la forma de los datos que viajan entre cliente y servidor. Con utility types defines todos a partir de un solo modelo:
ts
// Modelo completo (como está en la base de datos)
interface Producto {
id: string;
nombre: string;
descripcion: string;
precio: number;
stock: number;
categoria: 'ropa' | 'hogar' | 'tecnologia';
costoProveedor: number; // dato interno: nunca al público
creadoEn: Date;
actualizadoEn: Date;
}
// Campos que genera el servidor
type CamposAuto = 'id' | 'creadoEn' | 'actualizadoEn';
// POST /productos: el cliente manda todo menos los automáticos
type CrearProductoDto = Omit<Producto, CamposAuto>;
// PATCH /productos/:id: cualquier subconjunto de lo editable
type ActualizarProductoDto = Partial<CrearProductoDto>;
// GET /productos: sin datos internos
type ProductoPublicoDto = Omit<Producto, 'costoProveedor'>;
// Listado: solo lo necesario para la tarjeta
type ProductoResumenDto = Pick<Producto, 'id' | 'nombre' | 'precio'>;
// Funciones del servicio
async function crearProducto(datos: CrearProductoDto): Promise<ProductoPublicoDto> {
const res = await fetch('/api/productos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos)
});
return res.json();
}
async function actualizarProducto(id: string, cambios: ActualizarProductoDto) {
await fetch(`/api/productos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cambios)
});
}
actualizarProducto('p1', { precio: 499 }); // ✅
actualizarProducto('p1', { id: 'otro' }); // ❌ id no es editableSi mañana agregas marca: string a Producto, los DTOs se actualizan solos.
Introducción a tipos mapeados
¿Cómo funciona Partial por dentro? Es un tipo mapeado: recorre las claves de un tipo y transforma cada una.
ts
// Así está definido Partial (simplificado)
type MiPartial<T> = {
[K in keyof T]?: T[K];
};
// "Para cada clave K de T, crea una propiedad opcional con el mismo tipo"
// Uno propio: convertir todas las propiedades en string (para formularios)
type ComoFormulario<T> = {
[K in keyof T]: string;
};
type FormProducto = ComoFormulario<Pick<Producto, 'nombre' | 'precio' | 'stock'>>;
// { nombre: string; precio: string; stock: string }
// Otro: un objeto de errores de validación con las mismas claves
type Errores<T> = {
[K in keyof T]?: string;
};
const errores: Errores<FormProducto> = { precio: 'Debe ser mayor a 0' };Introducción a tipos condicionales
Un tipo condicional es un "if" a nivel de tipos: A extends B ? X : Y.
ts
type EsTexto<T> = T extends string ? 'sí' : 'no';
type A = EsTexto<'hola'>; // 'sí'
type B = EsTexto<42>; // 'no'
// Así está definido NonNullable
type MiNonNullable<T> = T extends null | undefined ? never : T;
// infer: "extrae" un tipo de dentro de otro
type ElementoDe<T> = T extends (infer E)[] ? E : never;
type X = ElementoDe<string[]>; // stringNo necesitas dominarlos para trabajar día a día; con saber leerlos es suficiente. Los utility types integrados cubren la gran mayoría de los casos.
Ejercicios
- A partir de
interface Tarea { id: number; titulo: string; completada: boolean; creadaEn: Date }, creaNuevaTarea(sinidnicreadaEn) yEditarTarea(todo lo deNuevaTarea, opcional). - Dada la unión
type Evento = 'click' | 'scroll' | 'keydown' | 'keyup' | 'resize', creaEventoTecladoconExtractyEventoNoTecladoconExclude. - Dada esta función, obtén el tipo de su resultado ya resuelto:
ts
async function cargarPerfil(id: number) {
return { id, nombre: 'Ana', seguidores: 120 };
}- Crea un tipo mapeado
Banderas<T>que convierta todas las propiedades deTenboolean.
Ver solución
ts
// 1
interface Tarea {
id: number;
titulo: string;
completada: boolean;
creadaEn: Date;
}
type NuevaTarea = Omit<Tarea, 'id' | 'creadaEn'>;
type EditarTarea = Partial<NuevaTarea>;
// 2
type Evento = 'click' | 'scroll' | 'keydown' | 'keyup' | 'resize';
type EventoTeclado = Extract<Evento, 'keydown' | 'keyup'>;
type EventoNoTeclado = Exclude<Evento, 'keydown' | 'keyup'>;
// 3
type Perfil = Awaited<ReturnType<typeof cargarPerfil>>;
// { id: number; nombre: string; seguidores: number }
// 4
type Banderas<T> = {
[K in keyof T]: boolean;
};
type CamposTocados = Banderas<NuevaTarea>;
// { titulo: boolean; completada: boolean }Resumen
- Los utility types derivan tipos nuevos de uno existente: una sola fuente de verdad.
Partial/Required/Readonlycambian modificadores de todas las propiedades.Picktoma propiedades;Omitlas quita. Ideales para DTOs.Exclude,ExtractyNonNullablefiltran uniones.ReturnType,ParametersyAwaitedextraen tipos de funciones (contypeof).- Los tipos mapeados (
[K in keyof T]) y condicionales (A extends B ? X : Y) son la base de todos ellos.
Siguiente: Narrowing