Skip to content

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 missing

Readonly ​

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>: quita null y undefined.
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 editable

Si 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[]>;  // string

No 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 ​

  1. A partir de interface Tarea { id: number; titulo: string; completada: boolean; creadaEn: Date }, crea NuevaTarea (sin id ni creadaEn) y EditarTarea (todo lo de NuevaTarea, opcional).
  2. Dada la unión type Evento = 'click' | 'scroll' | 'keydown' | 'keyup' | 'resize', crea EventoTeclado con Extract y EventoNoTeclado con Exclude.
  3. 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 };
}
  1. Crea un tipo mapeado Banderas<T> que convierta todas las propiedades de T en boolean.
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 / Readonly cambian modificadores de todas las propiedades.
  • Pick toma propiedades; Omit las quita. Ideales para DTOs.
  • Exclude, Extract y NonNullable filtran uniones.
  • ReturnType, Parameters y Awaited extraen tipos de funciones (con typeof).
  • Los tipos mapeados ([K in keyof T]) y condicionales (A extends B ? X : Y) son la base de todos ellos.

Siguiente: Narrowing

Academia Web — aprende, copia y construye.