Domina los tipos de utilidad integrados de TypeScript para un código más limpio
Aprenda cómo los tipos útiles de TypeScript, como Partial, Pick, Omit y Record, eliminan las interfaces duplicadas y mantienen automáticamente sincronizadas las definiciones de tipo.
Deje de copiar y pegar interfaces: escriba código más limpio e impenetrable con los transformadores de tipo integrados en TypeScript.
Tarde o temprano, todo desarrollador de TypeScript se topa con el mismo problema. Diseña una interfaz User limpia con ocho campos. Unos minutos después, necesita un formulario de edición en el que solo dos campos son realmente obligatorios, así que crea una interfaz UpdateUserInput. Luego llega una respuesta de API que debe ocultar campos sensibles, por lo que también agrega una interfaz PublicUser. Antes de darse cuenta, una buena parte de su código está compuesta por interfaces casi idénticas que, a medida que crece el proyecto, dejan de estar sincronizadas sin que se note.
Cada vez que cambia el esquema subyacente, se ve obligado a buscar y actualizar varios lugares al mismo tiempo. Si se olvida de uno, acaba creando un error que aparecerá más tarde en producción.
TypeScript resuelve esto con una herramienta integrada llamada Utility Types. Puedes pensar en ellas como funciones que operan sobre tipos en lugar de valores: introduces un tipo existente, aplicas alguna regla de transformación y obtienes un tipo completamente nuevo. Esto te permite mantener una definición única y autorizada, delegando todo el trabajo repetitivo de reestructuración al compilador.
A continuación se presentan los tipos útiles que vale la pena usar constantemente, explicados en lenguaje sencillo con ejemplos prácticos que puedes adaptar de inmediato.
1. Transformación de la estrictitud de las propiedades
Estas tres herramientas permiten controlar si los campos son opcionales, obligatorios o fijos.
Partial<Type>
En términos sencillos: "Hacer que todas las propiedades sean opcionales."
Cuando estás creando algo como un endpoint PATCH o actualizando parte del estado de una aplicación, no deseas solicitar todo el objeto al llamante. Partial<T> recorre cada campo de una interfaz y agrega un ? a cada uno.
interface UserProfile {
id: string;
name: string;
email: string;
avatarUrl: string;
theme: "light" | "dark";
}
// Without Partial, you'd have to rewrite all fields with '?'
type UpdateProfileInput = Partial<UserProfile>;
function updateProfile(userId: string, changes: UpdateProfileInput) {
// Valid: changes can include any subset of UserProfile properties
apiClient.patch(`/users/${userId}`, changes);
}
// Valid call:
updateProfile("usr_101", { theme: "dark" });
Required<Type>
En términos sencillos: "Elimina todos los marcadores opcionales para que nada pueda ser omitido."
Required<T> hace lo opuesto a Partial<T>: dado una interfaz donde algunos campos son opcionales, obliga a que todos estén presentes. Esto resulta útil cuando se integran configuraciones predeterminadas y es necesario garantizar que no falte nada.
interface AppConfig {
apiUrl: string;
timeout?: number;
retries?: number;
enableLogging?: boolean;
}
// Ensure the internal runner has every single setting resolved
type ResolvedConfig = Required<AppConfig>;
const defaultConfig: ResolvedConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
enableLogging: true,
};
function initializeApp(userOptions: AppConfig): ResolvedConfig {
return { ...defaultConfig, ...userOptions };
}
Readonly<Type>
En términos sencillos: "Congela cada propiedad para que no pueda ser reasignada."
Cuando deseas evitar mutaciones accidentales —piensa en valores de configuración, propiedades de componentes o estado compartido de la aplicación— las banderas Readonly<T> marcan cada propiedad como readonly. Cualquier intento de escribir en una de esas claves posteriormente falla en tiempo de compilación.
interface SystemRole {
roleName: string;
permissions: string[];
}
const SuperAdminRole: Readonly<SystemRole> = {
roleName: "SUPER_ADMIN",
permissions: ["read", "write", "delete", "admin"],
};
// Error: Cannot assign to 'roleName' because it is a read-only property.
SuperAdminRole.roleName = "USER";
2. Extraer lo que necesitas
Existen muchas situaciones en las que solo necesitas una parte de una interfaz. En lugar de crear un tipo completamente nuevo e independiente, puedes extraer partes directamente de tus modelos de dominio existentes.
Pick<Type, Keys>
En términos sencillos: "Crea un nuevo tipo utilizando únicamente estas claves nombradas del original."
Digamos que tienes un modelo grande, pero un widget de interfaz de usuario pequeño o una consulta a una base de datos ligera solo necesitan un nombre y un avatar. Puedes indicarle a TypeScript que mantenga únicamente esos dos campos.
interface Product {
id: string;
sku: string;
name: string;
price: number;
stockQuantity: number;
supplierId: string;
description: string;
}
// Pick only the fields a mini checkout card displays
type CartItemPreview = Pick<Product, "id" | "name" | "price">;
const item: CartItemPreview = {
id: "prod_99",
name: "Wireless Mechanical Keyboard",
price: 129.99,
};
Omit<Type, Keys>
En términos sencillos: "Conserva todo el tipo original, pero omite estos campos específicos."
Omit<T, K> invierte la función de Pick. Al insertar un nuevo registro en una base de datos, generalmente se incluyen todos los campos excepto aquellos que el sistema genera automáticamente, como id o createdAt.
interface Article {
id: number;
title: string;
slug: string;
content: string;
publishedAt: Date;
viewCount: number;
}
// Strip metadata handled by the database
type CreateArticlePayload = Omit<Article, "id" | "viewCount" | "publishedAt">;
function submitArticle(payload: CreateArticlePayload) {
// TypeScript guarantees no one accidentally submits an 'id' or 'viewCount'
apiClient.post("/articles", payload);
}
3. Mapeo dinámico de claves
Record<Keys, Type>
En términos sencillos: "Crea un diccionario cuyas claves provengan de esta lista y cuyos valores tengan esta estructura."
Los objetos simples de JavaScript utilizados como tablas de consulta suelen tener un tipo flexible, algo como { [key: string]: any }, lo que elimina la seguridad de tipo. En cambio, Record<Keys, Type> establece con precisión qué claves están permitidas y qué forma deben tener sus valores.
type SubscriptionTier = "free" | "pro" | "enterprise";
interface TierFeatures {
monthlyQuota: number;
customDomainAllowed: boolean;
supportLevel: "community" | "email" | "dedicated";
}
// Guarantees all three tiers are explicitly handled
const subscriptionPlans: Record<SubscriptionTier, TierFeatures> = {
free: {
monthlyQuota: 1000,
customDomainAllowed: false,
supportLevel: "community",
},
pro: {
monthlyQuota: 50000,
customDomainAllowed: true,
supportLevel: "email",
},
enterprise: {
monthlyQuota: 1000000,
customDomainAllowed: true,
supportLevel: "dedicated",
},
};
Si más adelante se agrega un nuevo nivel "starter" a SubscriptionTier, TypeScript marcará inmediatamente subscriptionPlans y le indicará que falta la entrada "starter">.
4. Recorte y reducción de tipos union
Las utilidades anteriores transforman las claves de los objetos. El siguiente par, en cambio, funciona con tipos union, como "apple" | "banana" | "orange".
Exclude<UnionType, ExcludedMembers>
En lenguaje sencillo: "Toma este conjunto de valores posibles y elimina los que se enumeran aquí."
type TaskStatus = "draft" | "in_progress" | "review" | "completed" | "archived";
// You can edit tasks in any status except when they are completed or archived
type EditableTaskStatus = Exclude<TaskStatus, "completed" | "archived">;
// Result: "draft" | "in_progress" | "review"
Extract<UnionType, ExtractedMembers>
En lenguaje sencillo: "Toma este conjunto de valores posibles y conserva solo aquellos que cumplen con estos criterios."
Extract funciona en la dirección opuesta a Exclude: aisla la intersección entre dos tipos union, manteniendo únicamente los miembros que existen en ambos.
type WindowEvents = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type PointerEvents = "click" | "mouseenter" | "mouseleave" | "mousemove";
// Keep only the events shared across both categories
type SharedEvents = Extract<WindowEvents, PointerEvents>;
// Result: "click" | "mousemove"
NonNullable<Type>
En términos sencillos: elimina los valores null y undefined de un tipo.
Cuando trabajas con datos provenientes de APIs externas o clientes de base de datos, los campos suelen estar definidos como string | null | undefined. NonNullable<T> elimina esas posibilidades de valores vacíos para que queden solo los tipos significativos.
type SearchQuery = string | string[] | null | undefined;
type CleanSearchQuery = NonNullable<SearchQuery>;
// Result: string | string[]
5. Inferir tipos a partir de código existente
Una de las mayores ventajas de TypeScript es poder obtener los tipos directamente de funciones, promesas y módulos importados, en lugar de declararlos manualmente.
ReturnType<FunctionType>
En términos sencillos: muestra qué devuelve una función, de modo que nunca es necesario volver a escribir esa estructura a mano.
Para funciones de fábrica complejas, selectores al estilo Redux o paquetes de terceros que no exponen la estructura de sus resultados, puedes obtener la estructura exacta haciendo referencia a la función en sí.
function buildSessionPayload(userId: string, roles: string[]) {
return {
sessionId: crypto.randomUUID(),
authenticatedAt: Date.now(),
expiresAt: Date.now() + 1000 * 60 * 60 * 24,
account: {
id: userId,
primaryRole: roles[0] ?? "guest",
allRoles: roles,
},
};
}
// Automatically inherits the shape of whatever buildSessionPayload returns
type UserSession = ReturnType<typeof buildSessionPayload>;
Si el objeto devuelto dentro de buildSessionPayload cambia posteriormente, UserSession detecta automáticamente ese cambio.
Parameters<FunctionType>
En términos sencillos: genera una tupla que enumera todos los argumentos que acepta una función.
Cada vez que envuelvas un método de SDK, reenvíes argumentos o crees una capa de registro alrededor de una llamada a función, Parameters<T> garantiza que la firma de tu wrapper nunca se desvíe de la original.
function triggerAlert(message: string, severity: "low" | "high", code?: number) {
// Internal dispatch logic
}
// Extracts arguments as a tuple: [message: string, severity: "low" | "high", code?: number]
type AlertArgs = Parameters<typeof triggerAlert>;
function logAndAlert(...args: AlertArgs) {
console.log("Dispatching alert:", args[0]);
triggerAlert(...args);
}
Awaited<Type>
En términos sencillos: desempaca una Promise y expone el valor que contiene.
Dado que gran parte del TypeScript moderno gira en torno al código asíncrono, Awaited<T> es esencial para ir desprendiendo una o más capas de promises anidados hasta llegar al valor realmente resuelto.
async function fetchAccountDetails() {
return {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
}
// Unwraps the Promise<T> returned by the async function
type AccountDetails = Awaited<ReturnType<typeof fetchAccountDetails>>;
// Result: { accountId: string; balance: number; currency: string }
const cachedAccount: AccountDetails = {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
6. Apilar tipos utilitarios como bloques de construcción
Los tipos utilitarios demuestran su verdadera fuerza cuando se combinan. Como cada uno genera un tipo estándar de TypeScript, puedes pasar directamente el resultado de uno a otro.
Considere una capa de datos construida en torno a la entidad Customer:
interface Customer {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber?: string;
createdAt: Date;
updatedAt: Date;
}
// 1. Creation payload: No auto-generated fields, but firstName, lastName, and email are mandatory
type CreateCustomerDTO = Omit<Customer, "id" | "createdAt" | "updatedAt">;
// 2. Update payload: Omit IDs, and let the user modify ANY valid field optionally
type UpdateCustomerDTO = Partial<Omit<Customer, "id" | "createdAt" | "updatedAt">>;
// 3. Read-only view for UI caches
type ReadonlyCustomer = Readonly<Customer>;
Con solo unas pocas líneas, has creado tres objetos de transferencia de datos separados y completamente verificados tipológicamente. Si la interfaz base Customer adquiere posteriormente una nueva propiedad —digamos, loyaltyTier—, los tres tipos derivados se actualizan por sí solos, sin necesidad de ediciones manuales.
Resumen de referencia rápida
Tratar los tipos como duplicados copiados a mano convierte cada cambio en el esquema en una carga de mantenimiento. Los tipos utilitarios te permiten definir tus modelos de negocio una sola vez y que el compilador genere todas las variaciones necesarias a partir de esa definición única. El resultado es un código más ligero, refactorizaciones más seguras y menos tiempo dedicado a armonizar interfaces casi idénticas, para que puedas concentrarte en desarrollar funcionalidades.
Lecturas relacionadas
- Modelado de dominios en TypeScript: Más allá de las anotaciones de tipos básicas — Aprenda hábitos prácticos en TypeScript, desde la diferencia entre “unknown” y “any”, pasando por las uniones discriminadas y la función “satisfies”, que le ayudarán a modelar estados válidos en lugar de simplemente etiquetar datos.
- Errores comunes en JavaScript y TypeScript que destruyen el código silenciosamente — Explica trampas sutiles en JavaScript y TypeScript, desde comparaciones con NaN hasta problemas de sincronización asíncrona y coerción de tipos, que causan errores a pesar de parecer correctos.