Mucha gente usa TypeScript sin haber escrito nunca un generic. El código compila, funciona, y parece suficiente. Pero cuando necesitas una función que funciona con cualquier tipo de dato, manteniendo type safety, los generics dejan de ser teoría y se vuelven necesidad.
Generics básicos: cuándo y por qué
Un generic es un parámetro de tipo. Igual que una función acepta parámetros de valor, un generic acepta parámetros de tipo. Su razón de existir: escribir una vez, tipar para cualquier tipo.
// Sin generic: necesitas una función para cada tipo
function getFirstString(arr: string[]): string | undefined { return arr[0] }
function getFirstNumber(arr: number[]): number | undefined { return arr[0] }
// Con generic: una función para todos
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
const a = getFirst(['a', 'b', 'c']) // tipo: string | undefined
const b = getFirst([1, 2, 3]) // tipo: number | undefined
TypeScript infiere el tipo del generic automáticamente. Cuando pasas un array de strings, T se convierte en string. Sin necesidad de declararlo.
Constraints: limitar lo que el generic acepta
A veces el generic necesita una garantía. "Funciona con cualquier tipo, pero ese tipo debe tener esta propiedad." Los constraints resuelven esto con extends:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Marc', age: 30 }
getProperty(user, 'name') // ok: devuelve string
getProperty(user, 'email') // error: 'email' no existe en user
keyof T devuelve un union de los nombres de las propiedades de T. K extends keyof T garantiza que la clave pasada existe en el objeto. El error ocurre en tiempo de compilación, no en runtime.
Conditional types: tipos que deciden
Los conditional types usan la sintaxis T extends U ? X : Y. El tipo resultante depende de una condición. Piensa en un ternario, pero para tipos.
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
El caso de uso real: derivar tipos a partir de inputs. Una API que retorna tipos diferentes según el parámetro.
type ApiResponse<T extends 'user' | 'product'> =
T extends 'user' ? { id: string; name: string; email: string }
: T extends 'product' ? { id: string; title: string; price: number }
: never
type UserResponse = ApiResponse<'user'> // { id: string; name: string; email: string }
type ProductResponse = ApiResponse<'product'> // { id: string; title: string; price: number }
Mapped types: transformar tipos en masa
Los mapped types iteran sobre las propiedades de un tipo y producen un nuevo tipo. La sintaxis es { [K in Keys]: NewType }.
// Hace todas las propiedades opcionales
type MyPartial<T> = { [K in keyof T]?: T[K] }
// Hace todas las propiedades readonly
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
// Hace propiedades específicas obligatorias
type WithRequired<T, K extends keyof T> =
T & Required<Pick<T, K>>
type User = { name?: string; email?: string; id?: string }
type UserWithId = WithRequired<User, 'id'>
// resultado: { name?: string; email?: string; id: string }
Utility types de TypeScript que ya usas (o deberías)
TypeScript ya viene con utility types listos para usar. Conocer los más usados evita reinventar la rueda:
type User = { id: string; name: string; email: string; role: 'admin' | 'user' }
// Pick: selecciona propiedades
type UserBasic = Pick<User, 'id' | 'name'>
// Omit: elimina propiedades
type CreateUser = Omit<User, 'id'>
// Record: crea un tipo con claves y valores tipados
type Roles = Record<string, { permissions: string[] }>
// Extract: extrae tipos de un union
type AdminRole = Extract<User['role'], 'admin'>
// Exclude: elimina tipos de un union
type NonAdmin = Exclude<User['role'], 'admin'> // 'user'
Patrón real: tipar una función que procesa callbacks
Un patrón que combina generics con constraints: una función que recibe callbacks con tipos relacionados al input.
function mapWithFallback<TInput, TOutput>(
items: TInput[],
transform: (item: TInput) => TOutput,
fallback: TOutput
): TOutput[] {
return items.map(item => {
try {
return transform(item)
} catch {
return fallback
}
})
}
// TypeScript infiere TInput = string, TOutput = number
const result = mapWithFallback(
['1', '2', 'abc'],
s => parseInt(s),
0
) // tipo: number[]
Cuándo dejar de complicar
Los generics y conditional types son poderosos, pero tienen un costo de legibilidad. Si estás escribiendo un tipo que nadie del equipo puede leer en 10 segundos, simplifica. Type safety importa, pero el mantenimiento importa más.
Usa generics cuando necesitas flexibilidad de tipos. Usa conditional types cuando tienes lógica de tipos genuina. Usa mapped types cuando necesitas transformar estructuras. Cuando nada de eso aplica, un any localizado y documentado es mejor que un tipo genial que nadie entiende.
¿Te gustó el contenido?
Construyo productos web y soluciones con IA de la manera correcta — arquitectura sólida, código sostenible y entrega real.
Hablemos