¿Qué son los tipos avanzados de TypeScript?
Los tipos avanzados son las herramientas que te permiten transformar, componer y manipular tipos a nivel de tipo. En lugar de definir manualmente tipos paralelos para cada variación, los derivas automáticamente de los ya existentes.
Si los conceptos básicos te proporcionan los tipos y los genéricos te permiten escribir código reutilizable, los tipos avanzados te permiten construir un sistema de tipos que describe tus datos con precisión, detectando errores que los enfoques más simples pasarían por alto.
Los tres pilares son los utility types (transformaciones integradas), los conditional types (lógica a nivel de tipo) y los mapped types (transformación de propiedades). Juntos, forman un pequeño lenguaje para trabajar con tipos.
Utility types
TypeScript incluye tipos integrados que permiten modificar otros tipos:
Partial y Required
interface User {
id: number;
name: string;
email: string;
}
// All properties optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; }
// All properties required
type StrictUser = Required<Partial<User>>;
// { id: number; name: string; email: string; }
Pick y Omit
// Extract specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Remove specific properties
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; }
Record
// Create object type with specific keys and values
type Scores = Record<string, number>;
// { [key: string]: number }
const scores: Scores = { math: 95, science: 87 };
// With union keys
type StatusMap = Record<"idle" | "loading" | "done", string>;
Extract y Exclude
type Status = "idle" | "loading" | "success" | "error";
// Keep only matching members
type ActiveStatus = Extract<Status, "loading" | "success">;
// "loading" | "success"
// Remove matching members
type InactiveStatus = Exclude<Status, "loading" | "success">;
// "idle" | "error"
ReturnType y Parameters
function createUser(name: string, age: number) {
return { name, age, id: Date.now() };
}
type User = ReturnType<typeof createUser>;
// { name: string; age: number; id: number }
type Args = Parameters<typeof createUser>;
// [string, number]
NonNullable
type Maybe = string | null | undefined;
type Definite = NonNullable<Maybe>;
// string
Mapped types
Los mapped types iteran sobre las claves de un tipo existente y transforman cada propiedad:
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
type MutableUser = ReadOnly<User>;
// All properties are readonly
Agregar modificadores
// Add optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Remove optional
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
Los operadores - y + eliminan o agregan modificadores durante el mapeo.
Remapeo de claves con as
TypeScript 4.1+ permite remapear las claves durante el mapeo:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string; }
Tipos condicionales
Los tipos condicionales permiten expresar lógica a nivel de tipos:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
La palabra clave infer
infer extrae un tipo desde el interior de otro tipo:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => string;
type Result = ReturnType<Fn>; // string
Ejemplos prácticos
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Num = ElementOf<number[]>; // number
// Unwrap promises
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Deep = Awaited<Promise<Promise<string>>>; // string
// Extract object property types
type ValuesOf<T> = T[keyof T];
type UserValues = ValuesOf<User>; // number | string
Tipos condicionales distributivos
Cuando un tipo condicional se distribuye sobre una unión, cada miembro se evalúa por separado:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] — not (string | number)[]
Esto es útil para tipos como Extract y Exclude que filtran miembros de una unión.
Tipos de template literal
Los tipos de template literal permiten manipular cadenas de texto a nivel de tipos:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
Con mapped types
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]: () => void;
};
type MouseEvents = EventHandlers<"click" | "hover" | "focus">;
// { onClick: () => void; onHover: () => void; onFocus: () => void; }
Coincidencia de patrones de cadenas (String pattern matching)
type CSSProperty = `${string}-${string}`;
type ValidCSS = CSSProperty; // any string with a hyphen
Uniones discriminadas
Las uniones discriminadas son el patrón avanzado más importante para modelar el estado:
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function handleState(state: RequestState<User>) {
switch (state.status) {
case "idle":
return "Ready";
case "loading":
return "Loading...";
case "success":
return state.data.name; // TypeScript knows data exists
case "error":
return state.error; // TypeScript knows error exists
}
}
La propiedad status es el discriminante. TypeScript reduce el tipo automáticamente en cada rama.
Branded types
Los branded types evitan que se mezclen tipos que tienen la misma estructura:
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
function branded<T extends string>(value: T): T & { readonly __brand: unique symbol } {
return value as any;
}
const userId = branded<UserId>("user-123");
const orderId = branded<OrderId>("order-456");
function getUser(id: UserId) { /* ... */ }
getUser(userId); // OK
getUser(orderId); // Error: OrderId is not assignable to UserId
Mejores prácticas
- Utiliza utility types en lugar de duplicar manualmente las definiciones de tipos.
- Prefiere las discriminated unions sobre las type assertions para el modelado de estados.
- Construye los tipos a partir de piezas pequeñas y componibles en lugar de definiciones monolíticas y extensas.
- Usa los conditional types con moderación; si un tipo es difícil de leer, simplifica la API.
- Utiliza
inferpara extraer tipos de librerías en lugar de tipar manualmente los valores de retorno. - Mantén la fuente de verdad en un solo lugar y deriva todo lo demás.
- Usa branded types cuando necesites nominal typing para IDs o valores similares.
- Trata los tipos complejos como último recurso: los tipos simples son más fáciles de mantener.
Errores comunes
- Sobre-diseñar los tipos cuando unos más simples funcionarían.
- Crear tipos condicionales profundamente anidados que resultan imposibles de depurar.
- Usar aserciones de
asen lugar de un estrechamiento de tipos (type narrowing) adecuado. - No utilizar tipos de utilidad, lo que provoca la duplicación de definiciones de tipos.
- Crear tipos demasiado complejos para el beneficio que aportan.
- Olvidar que los tipos se eliminan en tiempo de ejecución; no existe la comprobación de tipos en runtime.
- No documentar los tipos complejos con comentarios que expliquen su intención.
Qué aprender a continuación
Ahora ya comprendes los tipos avanzados: utility types, conditional types, mapped types y template literals. A partir de aquí, el siguiente paso natural es TypeScript para dominar los fundamentos: anotaciones, inferencia, interfaces y genéricos. Elige un proyecto, refactoriza algunos tipos y deja que la práctica haga el resto.