Los genericos de TypeScript son una de las caracteristicas mas poderosas del sistema de tipos. Te permiten escribir codigo flexible, reutilizable y seguro en tipos parametrizando tipos. En lugar de duplicar funciones o clases para cada tipo de dato, los genericos te permiten escribir una vez y usar con cualquier tipo manteniendo la seguridad de tipos completa. Esta guia cubre todo, desde lo basico hasta patrones avanzados.
Tu Primera Funcion Generica
El punto de partida clasico es la funcion identidad — una funcion que retorna exactamente lo que recibe. Sin genericos, perderas informacion de tipo usando any, o tendras que escribir funciones separadas para cada tipo.
Con genericos, defines un parametro de tipo (convencionalmente llamado T) que actua como marcador de posicion. Al llamar la funcion, TypeScript infiere el tipo real automaticamente.
// Without generics — loses type info
function identityAny(value: any): any {
return value;
}
const result1 = identityAny("hello"); // type: any (not string!)
// With generics — preserves type info
function identity<T>(value: T): T {
return value;
}
const result2 = identity("hello"); // type: string ✓
const result3 = identity(42); // type: number ✓
const result4 = identity(true); // type: boolean ✓
// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: stringFunciones Genericas
Las funciones genericas pueden aceptar multiples parametros de tipo e incluso tener tipos predeterminados. Esto las hace increiblemente versatiles.
// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("hello", 42); // type: [string, number]
// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]
// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strings = createArray(3, "x"); // T inferred as string
const numbers = createArray(3, 0); // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to stringLos parametros de tipo predeterminados funcionan como los parametros de funcion predeterminados — si el llamador no especifica un tipo, se usa el predeterminado.
Interfaces y Tipos Genericos
Los genericos no se limitan a funciones. Puedes parametrizar interfaces y alias de tipo para crear estructuras de datos flexibles.
// Generic interface
interface Box<T> {
value: T;
label: string;
}
const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };
// Generic interface with multiple params
interface KeyValuePair<K, V> {
key: K;
value: V;
}
// Generic type alias
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };
// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;
// Generic type for nullable values
type Nullable<T> = T | null | undefined;Las interfaces genericas son especialmente utiles para definir respuestas de API, colecciones y contenedores de datos.
Clases Genericas
Las clases tambien pueden ser genericas. Esto es util para construir estructuras de datos como pilas, colas, listas enlazadas o cualquier contenedor.
// Generic Stack class
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined
// Generic class with multiple type params
class HashMap<K, V> {
private map = new Map<K, V>();
set(key: K, value: V): void {
this.map.set(key, value);
}
get(key: K): V | undefined {
return this.map.get(key);
}
has(key: K): boolean {
return this.map.has(key);
}
}El parametro de tipo esta disponible en toda la clase — en propiedades, parametros de metodo y tipos de retorno.
Restricciones Genericas con extends
Los genericos sin restriccion aceptan cualquier tipo, pero a veces necesitas limitar los tipos permitidos. La palabra clave extends permite agregar restricciones.
Restringir a Claves de Objeto con keyof
Un patron muy comun es restringir un parametro de tipo a ser una clave valida de otro tipo.
// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name"); // type: string ✓
getProperty(user, "age"); // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"
// Pick specific keys from an object
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => { result[key] = obj[key]; });
return result;
}
const subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }Restringir a Formas Especificas
Puedes requerir que un parametro de tipo tenga ciertas propiedades extendiendo una interfaz o tipo objeto.
// Constraint to objects with a length property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): T {
console.log(`Length: ${value.length}`);
return value;
}
logLength("hello"); // OK — string has length
logLength([1, 2, 3]); // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42); // Error — number has no length property
// Constraint to objects with an id
interface HasId {
id: number | string;
}
function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
return items.find(item => item.id === id);
}
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const found = findById(users, 1);
// type: { id: number; name: string } | undefinedTipos Condicionales
Los tipos condicionales permiten elegir entre dos tipos basandose en una condicion, usando la sintaxis T extends U ? X : Y.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true
// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type D = Flatten<string[]>; // string
type E = Flatten<number[][]>; // number[]
type F = Flatten<string>; // string (not an array, returned as-is)
// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type G = MyReturnType<() => string>; // string
type H = MyReturnType<(x: number) => boolean>; // boolean
// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>; // numberLos tipos condicionales se vuelven especialmente poderosos combinados con infer, que permite extraer tipos de otros tipos.
Tipos Mapeados
Los tipos mapeados transforman las propiedades de un tipo existente para crear uno nuevo. Iteran sobre claves usando la sintaxis [K in keyof T].
// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties required (like Required<T>)
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Transform property types
type Stringify<T> = {
[K in keyof T]: string;
};
interface User {
name: string;
age: number;
active: boolean;
}
type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }
// Mapped type with conditional
type OptionalNullable<T> = {
[K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};Puedes agregar o eliminar modificadores como readonly y ? en tipos mapeados.
Tipos de Utilidad Incorporados
TypeScript incluye muchos tipos de utilidad construidos con genericos. Aqui estan los mas utilizados:
| Tipo de Utilidad | Descripcion | Ejemplo |
|---|---|---|
Partial<T> | Hace todas las propiedades opcionales | Partial<User> |
Required<T> | Hace todas las propiedades obligatorias | Required<Partial<User>> |
Pick<T, K> | Selecciona las propiedades especificadas | Pick<User, "name" | "email"> |
Omit<T, K> | Excluye las propiedades especificadas | Omit<User, "password"> |
Record<K, V> | Crea un mapeo de tipo clave-valor | Record<string, number> |
Readonly<T> | Hace todas las propiedades de solo lectura | Readonly<Config> |
ReturnType<T> | Extrae el tipo de retorno de una funcion | ReturnType<typeof fn> |
Parameters<T> | Extrae la tupla de tipos de parametros | Parameters<typeof fn> |
// Utility types in action
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// For update forms — all fields optional
type UpdateUser = Partial<User>;
// Public user — omit password
type PublicUser = Omit<User, "password">;
// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;
// Lookup table
type UserLookup = Record<number, User>;
// Immutable user from API
type FrozenUser = Readonly<User>;
// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>
type FetchParams = Parameters<typeof fetchUser>; // [number]Patrones del Mundo Real
Wrapper de Respuesta API Generico
Uno de los usos mas comunes de genericos es envolver respuestas de API en una estructura consistente.
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
};
}
// Usage with specific types
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}
// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓
const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓Props de Componente React Generico
Los genericos permiten construir componentes React que funcionan con diferentes tipos de datos manteniendo la seguridad de tipos.
// Generic list component props
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
onItemClick?: (item: T) => void;
emptyMessage?: string;
}
function GenericList<T>({
items,
renderItem,
keyExtractor,
onItemClick,
emptyMessage = "No items",
}: ListProps<T>) {
if (items.length === 0) return <p>{emptyMessage}</p>;
return (
<ul>
{items.map((item, index) => (
<li
key={keyExtractor(item)}
onClick={() => onItemClick?.(item)}
>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage — TypeScript infers T from items
<GenericList
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
onItemClick={(user) => console.log(user.email)}
/>Patron Repository Generico
El patron repository abstrae el acceso a datos detras de una interfaz generica, permitiendo cambiar fuentes de datos sin modificar la logica de negocio.
// Generic repository interface
interface Entity {
id: number | string;
}
interface Repository<T extends Entity> {
findAll(): Promise<T[]>;
findById(id: T["id"]): Promise<T | null>;
create(data: Omit<T, "id">): Promise<T>;
update(id: T["id"], data: Partial<T>): Promise<T>;
delete(id: T["id"]): Promise<boolean>;
}
// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
constructor(private baseUrl: string) {}
async findAll(): Promise<T[]> {
const res = await fetch(this.baseUrl);
return res.json();
}
async findById(id: T["id"]): Promise<T | null> {
const res = await fetch(`${this.baseUrl}/${id}`);
if (!res.ok) return null;
return res.json();
}
async create(data: Omit<T, "id">): Promise<T> {
const res = await fetch(this.baseUrl, {
method: "POST",
body: JSON.stringify(data),
});
return res.json();
}
async update(id: T["id"], data: Partial<T>): Promise<T> {
const res = await fetch(`${this.baseUrl}/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
return res.json();
}
async delete(id: T["id"]): Promise<boolean> {
const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
return res.ok;
}
}
// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]Emisor de Eventos Generico
Un emisor de eventos type-safe garantiza que los nombres de eventos y sus payloads esten correctamente tipados.
// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;
class TypedEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: EventCallback<Events[K]>[];
} = {};
on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
const cbs = this.listeners[event];
if (cbs) {
this.listeners[event] = cbs.filter(cb => cb !== callback);
}
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const cbs = this.listeners[event];
if (cbs) {
cbs.forEach(cb => cb(payload));
}
}
}
// Define your event types
interface AppEvents {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"item:added": { itemId: string; quantity: number };
"error": { message: string; code: number };
}
const emitter = new TypedEmitter<AppEvents>();
// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
console.log(payload.userId); // type: string ✓
console.log(payload.timestamp); // type: number ✓
});
// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});Errores Comunes
Aqui estan los errores mas frecuentes con genericos y sus correcciones:
| Error | Problema | Correccion |
|---|---|---|
| Usar any en lugar de genericos | Pierde toda la informacion de tipo | Usar parametro de tipo T en lugar de any |
| Sobre-restringir parametros de tipo | Hace el generico inutilizable para tipos validos | Solo restringir lo que realmente necesitas |
| Parametros de tipo innecesarios | Agregar T usado solo una vez aumenta la complejidad | Si T solo aparece en el tipo de retorno, usar el tipo concreto |
| No usar restricciones al acceder a propiedades | Error TypeScript: la propiedad no existe en el tipo T | Agregar restriccion extends: T extends { prop: type } |
| Confundir valores predeterminados con restricciones | El tipo predeterminado no restringe — los llamadores pueden pasar otros tipos | Usar extends para restringir, = para predeterminado |
| No inferir cuando es posible | Pasar tipos explicitamente cuando TS puede inferirlos | Dejar que TypeScript infiera tipos de los argumentos |
| Usar T[] en lugar de readonly T[] | Permite mutacion de arrays que deberian ser solo lectura | Usar readonly T[] para datos inmutables de APIs |
// ❌ Mistake: Using any
function badParse(json: string): any {
return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!
// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
try {
const parsed = JSON.parse(json);
return validator(parsed) ? parsed : null;
} catch {
return null;
}
}
// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
return arr.length; // T is never meaningfully used
}
// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
return arr.length;
}
// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
return obj.name; // Error: Property 'name' does not exist on type 'T'
}
// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
return obj.name; // OK ✓
}Preguntas Frecuentes
Cual es la diferencia entre genericos y el tipo any?
Los genericos preservan la informacion de tipo mientras que any la descarta. Con genericos, si pasas un string, TypeScript sabe que el retorno tambien es string. Con any, toda la informacion se pierde.
Cuando usar genericos vs sobrecargas de funciones?
Usa genericos cuando la logica es la misma para todos los tipos. Usa sobrecargas cuando la implementacion difiere segun el tipo de entrada.
Se pueden usar genericos con funciones flecha en archivos TSX?
Si, pero agrega una coma final para evitar ambiguedad JSX: const fn = <T,>(arg: T): T => arg.
Cuantos parametros de tipo deberia tener un generico?
Los menos posibles. Uno o dos es comun. Tres a veces es necesario. Mas de tres, considera refactorizar.
Los genericos afectan el rendimiento en tiempo de ejecucion?
No. Los genericos son puramente una caracteristica de tiempo de compilacion. Se eliminan completamente durante la compilacion sin overhead en tiempo de ejecucion.
Cual es la convencion de nomenclatura para parametros de tipo?
Las letras mayusculas individuales son convencionales: T para tipo, K para clave, V para valor, E para elemento, R para tipo de retorno.