En el desarrollo de aplicaciones escalables, la reutilización de código sin perder la seguridad de tipos juega un papel clave. Sin la programación genérica, los desarrolladores se ven obligados a duplicar estructuras idénticas para cada tipo de dato o a recurrir al tipo any, lo que destruye por completo la verificación estática.
Genéricos (Generics / Generalizaciones) — son parámetros para tipos. De la misma manera que las funciones normales aceptan argumentos de valor para calcular un resultado en tiempo de ejecución, los constructores de tipos aceptan parámetros de tipo (<T>) para calcular el tipo estático resultante en tiempo de compilación.
Sobre la base de los genéricos, la biblioteca estándar de TypeScript proporciona una familia de tipos utilitarios incorporados (Utility Types): Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, Omit<T, K>, Record<K, T>, ReturnType<T> y otros. Estas utilidades permiten transformar modelos de datos existentes sin duplicar interfaces manualmente.
Veamos la correspondencia entre el mundo de los valores y el mundo de los tipos:
+-------------------------------------------------------------------------+
| ANALOGÍA: VALORES vs TIPOS |
+-------------------------------------------------------------------------+
| MUNDO DE VALORES (Tiempo de Ejecución): |
| function wrapValue(val) { |
| return { data: val }; |
| } |
| wrapValue(42) ---> { data: 42 } |
+-------------------------------------------------------------------------+
| MUNDO DE TIPOS (Compilación): |
| type ApiResponse<T> = { |
| data: T; |
| status: number; |
| }; |
| ApiResponse<User> ---> { data: User; status: number } |
| ApiResponse<Post> ---> { data: Post; status: number } |
+-------------------------------------------------------------------------+
| TRANSFORMACIONES ESTÁNDAR (Utility Types): |
| Partial<User> ---> Todos los campos se vuelven opcionales (?) |
| Pick<User, 'id'> ---> Mantiene SÓLO las claves especificadas |
| Omit<User, 'pwd'> ---> Excluye las claves especificadas del tipo |
+-------------------------------------------------------------------------+<T>)
El desarrollador invoca la función `identity(42)` sin especificar explícitamente `identity<number>(42)`.
El compilador examina la posición del argumento y asocia el valor `42` con la variable de tipo `T`.
Si se declara `T extends Entity`, el compilador compara el tipo inferido con la restricción base.
El tipo `T` se fija, y todas las posiciones dependientes (tipo de retorno, otros argumentos) reciben el tipo exacto.
¿Qué papel desempeña la palabra clave `extends` en la declaración `function getEntityId<T extends { id: string }>(entity: T): string`?
apiClient.get<UserProfile>('/me'), que garantizan el tipo de datos de respuesta correcto.const [user, setUser] = useState<User | null>(null).Partial<User> para endpoints de solicitudes PATCH.Record<string, CacheEntry> o Record<Locale, Dictionary>.El compilador infiere automáticamente el tipo T basándose en el argumento pasado:
// Función genérica envoltorio
function wrapResult<T>(value: T): { success: true; payload: T } {
return { success: true, payload: value };
}
const numResult = wrapResult(100); // Tipo: { success: true; payload: number }
const strResult = wrapResult('OK'); // Tipo: { success: true; payload: string }
// Interfaz genérica de contenedor
interface PaginatedList<TItem> {
items: TItem[];
total: number;
page: number;
hasMore: boolean;
}
interface Article {
slug: string;
title: string;
}
type ArticleFeed = PaginatedList<Article>;Partial, Pick, Omit, Record#interface UserProfile {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// 1. Partial<T>: hace que todos los campos sean opcionales (para solicitudes PATCH)
type UpdateUserDto = Partial<UserProfile>;
// Equivalente a: { id?: string; name?: string; email?: string; role?: 'admin' | 'user'; createdAt?: Date }
// 2. Pick<T, K>: selecciona solo los campos especificados
type UserPreview = Pick<UserProfile, 'id' | 'name'>;
// Equivalente a: { id: string; name: string }
// 3. Omit<T, K>: excluye los campos especificados (por ejemplo, eliminamos id y createdAt al crear)
type CreateUserDto = Omit<UserProfile, 'id' | 'createdAt'>;
// Equivalente a: { name: string; email: string; role: 'admin' | 'user' }
// 4. Record<K, T>: crea un diccionario tipado clave-valor
type UserRegistry = Record<string, UserProfile>;
type RolePermissions = Record<'admin' | 'user', string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read'],
};keyof#Vincular dos parámetros de tipo permite crear helpers de acceso a propiedades absolutamente seguros:
// ¡K debe ser una clave del objeto T!
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const profile: UserProfile = {
id: 'usr_1',
name: 'Anna',
email: '[email protected]',
role: 'admin',
createdAt: new Date(),
};
const userEmail = getProperty(profile, 'email'); // ¡El tipo de userEmail es estrictamente string!
const creationDate = getProperty(profile, 'createdAt'); // ¡El tipo es estrictamente Date!
// ERROR DE COMPILACIÓN:
// getProperty(profile, 'nonExistentProperty');
// TS2345: Argument of type '"nonExistentProperty"' is not assignable to parameter of type 'keyof UserProfile'.¿Qué tipo resultará de aplicar `Omit<UserProfile, 'id' | 'role'>`, si `UserProfile` contiene los campos `id`, `name`, `email`, `role`, `createdAt`?
No crees genéricos por el simple hecho de crearlos:
// EXCESIVO Y MALO:
function printId<T extends string>(id: T): void {
console.log(id);
}
// CORRECTO Y CONCISO:
function printId(id: string): void {
console.log(id);
}Un genérico es necesario solo cuando existe una conexión lógica entre los tipos de varios argumentos o el tipo de valor de retorno.
Un patrón peligroso que desactiva las comprobaciones de tipo.
Solución: Utiliza Record<string, unknown> para valores desconocidos con validación posterior, o tipos estrictos.