TypeScript Avanzado: Generics, Utility Types y Type Guards
TypeScript es mucho más que añadir tipos a JavaScript. Su sistema de tipos es tan potente que funciona como un lenguaje de programación en sí mismo. Dominar sus características avanzadas te permite escribir código más seguro, expresivo y autodocumentado.
Generics: Código reutilizable con tipos
Los genéricos permiten crear funciones, clases e interfaces que funcionan con cualquier tipo manteniendo la seguridad de tipos.
El problema sin genéricos
// ❌ Sin genéricos: perdemos información de tipo
function getFirst(arr: any[]): any {
return arr[0];
}
const num = getFirst([1, 2, 3]); // tipo: any 😞
const str = getFirst(['a', 'b']); // tipo: any 😞
// ✅ Con genéricos: el tipo se preserva
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const num = getFirst([1, 2, 3]); // tipo: number 🎉
const str = getFirst(['a', 'b']); // tipo: string 🎉
Restricciones con extends
Puedes limitar qué tipos acepta un genérico:
// Solo acepta objetos que tengan 'id' y 'name'
interface HasId {
id: string | number;
}
function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
return items.find(item => item.id === id);
}
// Funciona con cualquier tipo que tenga 'id'
interface User { id: number; name: string; email: string; }
interface Product { id: string; name: string; price: number; }
const user = findById(users, 1); // tipo: User | undefined
const product = findById(products, 'abc'); // tipo: Product | undefined
Genéricos en componentes Angular
// Componente de tabla genérico
@Component({
selector: 'app-data-table',
template: `
<table>
<thead>
<tr>
@for (col of columns(); track col.key) {
<th>{{ col.label }}</th>
}
</tr>
</thead>
<tbody>
@for (row of data(); track trackBy()(row)) {
<tr>
@for (col of columns(); track col.key) {
<td>{{ row[col.key] }}</td>
}
</tr>
}
</tbody>
</table>
`
})
export class DataTableComponent<T extends Record<string, any>> {
data = input.required<T[]>();
columns = input.required<{ key: keyof T; label: string }[]>();
trackBy = input<(item: T) => any>(() => (item: any) => item.id);
}
Utility Types: Los tipos incluidos
TypeScript incluye tipos de utilidad que transforman tipos existentes. Son herramientas esenciales que deberías usar a diario:
Partial<T> — Todas las propiedades opcionales
interface User {
name: string;
email: string;
age: number;
}
// Perfecto para funciones de actualización parcial
function updateUser(id: string, changes: Partial<User>): void {
// changes puede tener cualquier combinación de name, email, age
}
updateUser('1', { name: 'German' }); // ✅
updateUser('1', { email: 'a@b.com', age: 28 }); // ✅
updateUser('1', {}); // ✅
Pick<T, K> y Omit<T, K> — Seleccionar/excluir propiedades
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Para la UI no queremos exponer el password
type PublicUser = Omit<User, 'password'>;
// Para un listado solo necesitamos id y name
type UserListItem = Pick<User, 'id' | 'name'>;
// Para crear un usuario no tenemos id ni createdAt
type CreateUserDto = Omit<User, 'id' | 'createdAt'>;
Record<K, V> — Diccionarios tipados
type UserRole = 'admin' | 'editor' | 'viewer';
interface RolePermissions {
canEdit: boolean;
canDelete: boolean;
canPublish: boolean;
}
// Garantiza que TODOS los roles están definidos
const permissions: Record<UserRole, RolePermissions> = {
admin: { canEdit: true, canDelete: true, canPublish: true },
editor: { canEdit: true, canDelete: false, canPublish: true },
viewer: { canEdit: false, canDelete: false, canPublish: false },
// Si olvido un rol, TypeScript me avisa 🎯
};
ReturnType<T> y Parameters<T> — Inferir de funciones
function createUser(name: string, email: string, role: UserRole) {
return { id: crypto.randomUUID(), name, email, role, createdAt: new Date() };
}
// Inferir el tipo de retorno sin definirlo manualmente
type User = ReturnType<typeof createUser>;
// { id: string; name: string; email: string; role: UserRole; createdAt: Date }
// Inferir los parámetros
type CreateUserParams = Parameters<typeof createUser>;
// [string, string, UserRole]
Type Guards: Estrechamiento de tipos
Los Type Guards permiten a TypeScript entender qué tipo específico tiene una variable en un punto del código.
Type Guard personalizado con is
interface Dog { kind: 'dog'; bark(): void; }
interface Cat { kind: 'cat'; meow(): void; }
type Animal = Dog | Cat;
// Type guard con tipo predicado
function isDog(animal: Animal): animal is Dog {
return animal.kind === 'dog';
}
function handleAnimal(animal: Animal) {
if (isDog(animal)) {
animal.bark(); // TypeScript sabe que es Dog aquí
} else {
animal.meow(); // TypeScript sabe que es Cat aquí
}
}
Discriminated Unions (Uniones discriminadas)
Un patrón muy poderoso para manejar estados de forma segura:
type ApiResponse<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function handleResponse<T>(response: ApiResponse<T>) {
switch (response.status) {
case 'loading':
return 'Cargando...';
case 'success':
return response.data; // TypeScript sabe que 'data' existe
case 'error':
return response.error; // TypeScript sabe que 'error' existe
}
}
Template Literal Types
Permiten crear tipos a partir de templates de strings:
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
// tipo: 'onClick' | 'onFocus' | 'onBlur'
// Útil para CSS-in-TS
type Size = 'sm' | 'md' | 'lg';
type Breakpoint = 'mobile' | 'tablet' | 'desktop';
type ResponsiveClass = `${Breakpoint}:${Size}`;
// 'mobile:sm' | 'mobile:md' | ... | 'desktop:lg'
Tipos condicionales
Permiten crear tipos que dependen de condiciones:
// Si T es un array, extraer el tipo de sus elementos
type UnwrapArray<T> = T extends Array<infer U> ? U : T;
type A = UnwrapArray<string[]>; // string
type B = UnwrapArray<number>; // number
// Hacer propiedades nullable opcionales
type NullableToOptional<T> = {
[K in keyof T as null extends T[K] ? K : never]?: T[K];
} & {
[K in keyof T as null extends T[K] ? never : K]: T[K];
};
Conclusión
TypeScript avanzado es una inversión que se paga sola. Cada tipo bien definido es un bug que nunca llegará a producción. Empieza añadiendo genéricos a tus funciones de utilidad, usa Utility Types para no repetir interfaces, y adopta discriminated unions para manejar estados de forma segura.