Tipos de Utilidad en TypeScript: Partial, Pick, Omit, Record
1. Descripción general
Cuando escribes TypeScript, te encontrarás con situaciones donde necesitas un tipo similar al que ya tienes, pero le faltan o le sobran algunos campos. En lugar de declarar una nueva interfaz, TypeScript proporciona un grupo de tipos de utilidad para transformar tipos existentes. Este artículo cubre los cuatro tipos de utilidad más usados: Partial, Pick, Omit y Record, con ejemplos prácticos y algunos errores comunes. Después de leerlo, sabrás elegir el tipo de utilidad correcto para cada situación en lugar de reescribir interfaces manualmente.
2. Partial
Partial<T> convierte todos los campos de T en opcionales. Es útil al escribir funciones de actualización, donde el llamador solo necesita pasar algunos campos.
interface Customer {
id: number;
name: string;
email: string;
phone: string;
}
function updateCustomer(id: number, changes: Partial<Customer>): Customer {
const current = findCustomerById(id); // asumir que esta función ya existe
return { ...current, ...changes };
}
updateCustomer(1, { email: "new@mail.com" });
Partial<Customer> crea un tipo equivalente a:
interface PartialCustomer {
id?: number;
name?: string;
email?: string;
phone?: string;
}
Gracias a esto, updateCustomer acepta un objeto que solo tiene email sin reportar error por campos faltantes.
Nota:
Partialsolo hace opcional a nivel 1 (superficial). SiCustomertiene campos de objetos anidados, los campos internos seguirán siendo obligatorios. Si quieres partial profundo, debes escribir tu propio tipo recursivo o usar una librería comotype-fest.
3. Pick<T, K>
Pick<T, K> selecciona un subconjunto de campos de T según la lista de claves K. Úsalo cuando solo necesites una parte de los datos de un tipo grande, como un DTO que devuelvas al cliente.
interface Order {
id: string;
customerId: number;
totalAmount: number;
items: string[];
createdAt: Date;
internalNote: string;
}
type OrderSummary = Pick<Order, "id" | "totalAmount" | "createdAt">;
const summary: OrderSummary = {
id: "ORD-001",
totalAmount: 250_000,
createdAt: new Date(),
};
OrderSummary solo tiene 3 campos. Si asignas un campo adicional items o internalNote a summary, el compilador reportará un error Object literal may only specify known properties.
3.1. Pick con unión de claves
Las claves pasadas a Pick son una unión de literales de string, así que puedes extraerlas en una variable separada para reutilizarlas:
type OrderPublicFields = "id" | "totalAmount" | "createdAt";
type OrderSummary = Pick<Order, OrderPublicFields>;
type OrderListItem = Pick<Order, OrderPublicFields | "customerId">;
Este enfoque evita repetir la lista de claves en muchos lugares cuando tienes muchas variaciones de DTO de la misma entidad.
4. Omit<T, K>
Omit<T, K> hace lo opuesto a Pick: elimina las claves especificadas y mantiene el resto. Se usa generalmente cuando quieres ocultar campos sensibles o campos internos.
interface User {
id: number;
username: string;
passwordHash: string;
role: string;
}
type PublicUser = Omit<User, "passwordHash">;
function toPublicUser(user: User): PublicUser {
const { passwordHash, ...rest } = user;
return rest;
}
La línea const { passwordHash, ...rest } = user extrae passwordHash por separado, rest solo contiene id, username, role — que coincide exactamente con el tipo PublicUser.
// Output al hacer console.log(toPublicUser(user))
{ id: 1, username: "nonla_dev", role: "ADMIN" }
Nota:
Omitse define basándose enPickyExclude, funciona bien con tipos de objeto literal, pero no elimina adecuadamente las sobrecargas o uniones discriminadas en algunos casos complejos. Con tipos unión (type A = B | C),Omit<A, K>fusionará los miembros antes de hacer omit, puede perder la propiedad discriminante. Cuando encuentres uniones complejas, considera escribir el tipo manualmente en lugar de usarOmitdirectamente.
5. Record<K, T>
Record<K, T> crea un tipo de objeto con todas las claves pertenecientes a K, donde cada clave tiene un valor de tipo T. Es útil cuando modelás mapas/diccionarios con claves fijas.
type PaymentStatus = "PENDING" | "PAID" | "CANCELLED";
interface StatusInfo {
label: string;
color: string;
}
const statusDisplay: Record<PaymentStatus, StatusInfo> = {
PENDING: { label: "Esperando pago", color: "orange" },
PAID: { label: "Pagado", color: "green" },
CANCELLED: { label: "Cancelado", color: "gray" },
};
function getStatusLabel(status: PaymentStatus): string {
return statusDisplay[status].label;
}
Como statusDisplay declara el tipo Record<PaymentStatus, StatusInfo>, el compilador te obliga a tener los tres campos PENDING, PAID, CANCELLED. Si olvidas una clave, TypeScript reporta un error en tiempo de compilación — esta es una ventaja sobre usar { [key: string]: StatusInfo } normal (la firma de índice no verifica que todas las claves estén presentes).
5.1. Record con claves dinámicas
Cuando la clave no es una unión literal sino string genérico, Record<string, T> es equivalente a una firma de índice:
const cache: Record<string, number> = {};
cache["order-1"] = 100;
En este caso ya no hay verificación de “todas las claves”, porque string tiene infinitos valores. Solo usa Record<string, T> cuando realmente necesites un diccionario libre, en otros casos deberías preferir uniones literales para aprovechar la verificación exhaustiva.
6. Combinando múltiples tipos de utilidad
Los tipos de utilidad pueden anidarse entre sí para crear tipos más complejos sin necesidad de escribir nuevas interfaces.
interface Product {
id: string;
name: string;
price: number;
stock: number;
supplierNote: string;
}
type ProductUpdateRequest = Partial<Omit<Product, "id">>;
function updateProduct(id: string, data: ProductUpdateRequest) {
// llamar a repository.update(id, data)
}
updateProduct("P001", { price: 199_000 });
Omit<Product, "id"> elimina el campo id (no se permite modificar la clave primaria), luego Partial hace que los campos restantes sean opcionales. Resultado: el llamador solo necesita pasar los campos que quiere cambiar, no se le permite pasar id.
7. Escribir pruebas para verificar el comportamiento
Dado que los tipos de utilidad solo existen en tiempo de compilación, la forma de verificar es escribir pruebas unitarias para las funciones que usan esos tipos, asegurando que el comportamiento en tiempo de ejecución es el esperado. Por ejemplo, usando Vitest:
<!-- Dependencias en package.json equivalentes (no es Maven) -->
import { describe, it, expect } from "vitest";
interface User {
id: number;
username: string;
passwordHash: string;
role: string;
}
type PublicUser = Omit<User, "passwordHash">;
function toPublicUser(user: User): PublicUser {
const { passwordHash, ...rest } = user;
return rest;
}
describe("toPublicUser", () => {
it("elimina passwordHash del resultado", () => {
const user: User = {
id: 1,
username: "nonla_dev",
passwordHash: "hash-123",
role: "ADMIN",
};
const result = toPublicUser(user);
expect(result).toEqual({
id: 1,
username: "nonla_dev",
role: "ADMIN",
});
expect(result).not.toHaveProperty("passwordHash");
});
});
Esta prueba confirma que la función toPublicUser no solo es correcta en términos de tipo, sino también correcta en tiempo de ejecución — passwordHash realmente desaparece del objeto devuelto, no solo se “oculta” en la capa de tipos.
8. Errores comunes y cuándo no usarlos
- Confundir
Pick/Omitcon la eliminación real de datos: ambos solo cambian el tipo, no afectan el objeto en tiempo de ejecución. La desestructuración (const { x, ...rest } = obj) es lo que realmente elimina el campo de los datos. - Abusar de
Partialpara objetos devueltos por API: si la respuesta siempre tiene todos los campos, usarPartialforzará al código llamador a verificarundefinedinnecesariamente en todas partes. Solo usaPartialpara entrada (update/patch), no deberías usarlo para salida que ya está garantizada que esté completa. Omitcon tipos unión complejos: como se mencionó en la sección 4,Omitpuede perder uniones discriminadas. Si tu tipo es una unión de múltiples variantes (por ejemploShape = Circle | Square), escribe el tipo manualmente o usa técnicas de mapped type condicionales en lugar de usarOmitdirectamente.Record<string, T>en lugar de interfaz clara: si conoces todas las claves de antemano, declara una unión literal y usaRecord<Key, T>para tener verificación exhaustiva. UsarRecord<string, T>indiscriminadamente perderá la capacidad de detectar claves faltantes/adicionales en tiempo de compilación.
Nota: Todos estos tipos de utilidad solo funcionan en tiempo de compilación. Después de compilar a JavaScript, desaparecen por completo — sin sobrecarga en tiempo de ejecución, pero tampoco hay validación en tiempo de ejecución. Si los datos provienen de fuera (API, archivo JSON), aún necesitas validarlos usando librerías como Zod o io-ts.
9. Conclusión
Partial, Pick, Omit y Record te ayudan a reutilizar tipos en lugar de reescribir interfaces duplicadas. Partial es adecuado para entrada de tipo update, Pick/Omit se usan para crear DTOs de entidades grandes, mientras que Record es ideal para diccionarios con claves fijas, aprovechando la verificación exhaustiva del compilador. Entender claramente las limitaciones de cada tipo — especialmente el comportamiento superficial de Partial y el problema con tipos unión de Omit — te ayudará a evitar errores de tipo difíciles de depurar más adelante.