En esta página

Utility types

12 min lectura TextoCap. 4 — Tipos avanzados

Los utility types: transformaciones de tipos listas para usar

TypeScript incluye en su librería estándar un conjunto de utility types (tipos utilitarios) que transforman tipos existentes de maneras comunes. En lugar de reescribir variantes de tus interfaces para cada caso de uso, los utility types te permiten derivarlas de forma declarativa y mantenible.

Conocerlos bien te ahorrará horas de trabajo y mantendrá tu código DRY (Don't Repeat Yourself) en la capa de tipos.


Partial\

Convierte todas las propiedades de T en opcionales:

interface Configuracion {
  tema: "claro" | "oscuro";
  idioma: string;
  notificaciones: boolean;
  itemsPorPagina: number;
}

type ConfiguracionParcial = Partial<Configuracion>;
// Equivale a:
// {
//   tema?: "claro" | "oscuro";
//   idioma?: string;
//   notificaciones?: boolean;
//   itemsPorPagina?: number;
// }

function actualizarConfiguracion(
  actual: Configuracion,
  cambios: Partial<Configuracion>
): Configuracion {
  return { ...actual, ...cambios };
}

const configBase: Configuracion = {
  tema: "oscuro",
  idioma: "es",
  notificaciones: true,
  itemsPorPagina: 20,
};

const nuevaConfig = actualizarConfiguracion(configBase, { tema: "claro" });

Required\

Hace que todas las propiedades opcionales se vuelvan obligatorias. Es el inverso de Partial:

interface OpcionesConexion {
  host?: string;
  puerto?: number;
  ssl?: boolean;
  timeout?: number;
}

type ConexionResuelta = Required<OpcionesConexion>;
// Todas las propiedades son ahora obligatorias

function conectar(opciones: Required<OpcionesConexion>): string {
  const protocolo = opciones.ssl ? "https" : "http";
  return `${protocolo}://${opciones.host}:${opciones.puerto}`;
}

const defaults: Required<OpcionesConexion> = {
  host: "localhost",
  puerto: 5432,
  ssl: false,
  timeout: 5000,
};

Readonly\

Hace que todas las propiedades de T sean de solo lectura (no se pueden reasignar):

interface Punto {
  x: number;
  y: number;
}

function reflejar(punto: Readonly<Punto>): Punto {
  // punto.x = 0; // ❌ Error: no se puede asignar a propiedad de solo lectura
  return { x: -punto.x, y: -punto.y };
}

const origen: Readonly<Punto> = { x: 0, y: 0 };

Readonly es útil para marcar parámetros de funciones que no deben ser mutados, comunicando intención y permitiendo que TypeScript lo verifique.


Pick\

Crea un nuevo tipo seleccionando solo las propiedades K de T:

interface Articulo {
  id: string;
  titulo: string;
  contenido: string;
  autor: string;
  etiquetas: string[];
  vistas: number;
  publicadoEn: Date;
}

// Solo los campos necesarios para una tarjeta de vista previa
type PreviewArticulo = Pick<Articulo, "id" | "titulo" | "autor" | "publicadoEn">;
// { id: string; titulo: string; autor: string; publicadoEn: Date }

Omit\

El complemento de Pick: crea un tipo con todas las propiedades de T excepto las indicadas en K:

// En operaciones de creación, el id lo genera el servidor
type CrearArticuloDto = Omit<Articulo, "id" | "vistas" | "publicadoEn">;
// { titulo: string; contenido: string; autor: string; etiquetas: string[] }

Record\

Crea un tipo de objeto con claves de tipo K y valores de tipo V:

type Estado = "pendiente" | "activo" | "inactivo" | "eliminado";

type DescripcionEstados = Record<Estado, string>;

const descripciones: DescripcionEstados = {
  pendiente:  "Esperando aprobación",
  activo:     "Operativo y visible",
  inactivo:   "Temporalmente desactivado",
  eliminado:  "Marcado para eliminación",
};

// También útil para cachés y mapas
type Cache<T> = Record<string, T>;

const cacheUsuarios: Cache<{ nombre: string; email: string }> = {};

Extract\ y Exclude\

Extract mantiene solo los tipos de T que son asignables a U. Exclude los elimina:

type Primitivos = string | number | boolean | null | undefined;

type SoloCadenas   = Extract<Primitivos, string>;            // string
type SinNulables   = Exclude<Primitivos, null | undefined>;  // string | number | boolean

type Evento = "click" | "focus" | "blur" | "change" | "submit";
type EventosTeclado = "keydown" | "keyup" | "keypress";

type EventosTodos = Evento | EventosTeclado;
type EventosSoloRaton = Exclude<EventosTodos, EventosTeclado>;
// "click" | "focus" | "blur" | "change" | "submit"

NonNullable\

Elimina null y undefined de un tipo:

type ValorNulable = string | number | null | undefined;
type ValorSeguro  = NonNullable<ValorNulable>; // string | number

function procesarValor(valor: string | null | undefined): string {
  const seguro: NonNullable<typeof valor> = valor ?? "";
  return seguro.toUpperCase();
}

ReturnType\ y Parameters\

Extraen el tipo de retorno y los parámetros de una función, respectivamente:

function buscarUsuario(id: number, incluirPerfil: boolean) {
  return {
    id,
    nombre: "Ejemplo",
    perfil: incluirPerfil ? { bio: "..." } : null,
  };
}

type ResultadoBusqueda = ReturnType<typeof buscarUsuario>;
// { id: number; nombre: string; perfil: { bio: string } | null }

type ParamsBusqueda = Parameters<typeof buscarUsuario>;
// [id: number, incluirPerfil: boolean]

// Útil para wrappers y proxies
function buscarConLog(...args: Parameters<typeof buscarUsuario>): ReturnType<typeof buscarUsuario> {
  console.log("Buscando usuario con args:", args);
  return buscarUsuario(...args);
}

ConstructorParameters\

Similar a Parameters, pero para constructores de clases:

class ConexionDB {
  constructor(
    private host: string,
    private puerto: number,
    private ssl: boolean
  ) {}
}

type ArgsConexion = ConstructorParameters<typeof ConexionDB>;
// [host: string, puerto: number, ssl: boolean]

function crearConexion(...args: ConstructorParameters<typeof ConexionDB>): ConexionDB {
  return new ConexionDB(...args);
}

Awaited\

Resuelve recursivamente el tipo de una Promise (o cualquier thenable):

async function obtenerDatos(): Promise<string[]> {
  return ["dato1", "dato2"];
}

type Resultado = Awaited<ReturnType<typeof obtenerDatos>>;
// string[]

// Casos anidados:
type PromesaAnidada = Awaited<Promise<Promise<number>>>;
// number

Ejemplo completo: servicio CRUD con utility types

// ──────────────────────────────────────────────────────────────
// Entidad base
// ──────────────────────────────────────────────────────────────
interface Tarea {
  id: string;
  titulo: string;
  descripcion: string;
  completada: boolean;
  prioridad: "baja" | "media" | "alta";
  etiquetas: string[];
  creadaEn: Date;
  actualizadaEn: Date;
}

// ──────────────────────────────────────────────────────────────
// DTOs derivados — sin duplicar propiedades
// ──────────────────────────────────────────────────────────────
type CrearTareaDto    = Omit<Tarea, "id" | "creadaEn" | "actualizadaEn">;
type ActualizarTareaDto = Partial<Pick<Tarea, "titulo" | "descripcion" | "completada" | "prioridad" | "etiquetas">>;
type TareaPublica     = Readonly<Omit<Tarea, "actualizadaEn">>;
type ResumenTarea     = Pick<Tarea, "id" | "titulo" | "completada" | "prioridad">;

// ──────────────────────────────────────────────────────────────
// Servicio genérico de repositorio
// ──────────────────────────────────────────────────────────────
class RepositorioTareas {
  private store: Record<string, Tarea> = {};

  crear(dto: CrearTareaDto): TareaPublica {
    const tarea: Tarea = {
      ...dto,
      id: crypto.randomUUID(),
      creadaEn: new Date(),
      actualizadaEn: new Date(),
    };
    this.store[tarea.id] = tarea;
    const { actualizadaEn: _descartada, ...publica } = tarea;
    return publica as TareaPublica;
  }

  obtener(id: string): TareaPublica | null {
    const tarea = this.store[id];
    if (!tarea) return null;
    const { actualizadaEn: _descartada, ...publica } = tarea;
    return publica as TareaPublica;
  }

  actualizar(id: string, cambios: ActualizarTareaDto): TareaPublica | null {
    const tarea = this.store[id];
    if (!tarea) return null;
    const actualizada: Tarea = { ...tarea, ...cambios, actualizadaEn: new Date() };
    this.store[id] = actualizada;
    const { actualizadaEn: _descartada, ...publica } = actualizada;
    return publica as TareaPublica;
  }

  listar(): ResumenTarea[] {
    return Object.values(this.store).map(({ id, titulo, completada, prioridad }) => ({
      id,
      titulo,
      completada,
      prioridad,
    }));
  }

  eliminar(id: string): boolean {
    if (!this.store[id]) return false;
    delete this.store[id];
    return true;
  }
}

// ──────────────────────────────────────────────────────────────
// Prueba rápida
// ──────────────────────────────────────────────────────────────
const repo = new RepositorioTareas();

const nueva = repo.crear({
  titulo: "Estudiar utility types",
  descripcion: "Completar la lección de TypeScript",
  completada: false,
  prioridad: "alta",
  etiquetas: ["typescript", "aprendizaje"],
});

console.log("Creada:", nueva.titulo);

const actualizada = repo.actualizar(nueva.id, { completada: true });
console.log("¿Completada?", actualizada?.completada); // true

console.log("Resumen:", repo.listar());

Cuándo usar cada utility type

Utility type Úsalo cuando...
Partial<T> Actualización parcial de entidades (PATCH)
Required<T> Tienes un tipo con opcionales y necesitas garantizar todos
Readonly<T> Parámetros que no deben mutarse (props de componentes)
Pick<T, K> Necesitas un subconjunto de propiedades de una entidad
Omit<T, K> Quieres todo excepto unos pocos campos (ej: id generado)
Record<K, V> Mapas, cachés, configuraciones indexadas
Extract<T, U> Filtrar una unión para quedarte con tipos específicos
Exclude<T, U> Quitar tipos de una unión
NonNullable<T> Asegurar que un tipo no sea null ni undefined
ReturnType<T> Derivar el tipo de retorno sin importarlo explícitamente
Parameters<T> Crear wrappers que reenvían los mismos argumentos
Awaited<T> Obtener el tipo resuelto de una Promise

En la siguiente lección profundizarás en tipos condicionales y mapped types, que son precisamente los mecanismos internos con los que todos estos utility types están implementados.

Combina utility types para mayor precisión
Los utility types se pueden anidar: `Partial<Omit<T, 'id'>>` crea un tipo donde todos los campos excepto id son opcionales. La composición es la clave para evitar duplicar definiciones de tipos.
Awaited resuelve Promises anidadas
A diferencia de versiones anteriores, `Awaited<T>` resuelve recursivamente: `Awaited<Promise<Promise<string>>>` da `string`. Úsalo para tipar correctamente el resultado de funciones async.