Tipos Genericos en TypeScript: Guia Completa con Ejemplos Practicos
Aprende a usar tipos genericos en TypeScript paso a paso. Funciones, interfaces, constraints, utility types y patrones avanzados en componentes React con ejemplos reales.
Tipos Genericos en TypeScript: Guia Completa
Los tipos genericos en TypeScript te permiten escribir funciones, interfaces y clases que trabajan con cualquier tipo de dato sin sacrificar la seguridad del tipado estatico. Son la herramienta que separa el codigo TypeScript que simplemente compila del codigo TypeScript que realmente aprovecha el sistema de tipos.
Si alguna vez escribiste una funcion y la tipaste con any porque no sabias como hacerla funcionar con distintos tipos, los genericos son exactamente lo que necesitas.
Que son los genericos y por que importan
Un generico es un parametro de tipo. Igual que una funcion recibe parametros de valores, un generico recibe parametros de tipos.
Sin genericos, tienes dos opciones malas:
// Opcion 1: Funcion especifica para cada tipo
function primerElementoString(arr: string[]): string {
return arr[0]
}
function primerElementoNumber(arr: number[]): number {
return arr[0]
}
// Opcion 2: Usar any (pierdes el tipado)
function primerElemento(arr: any[]): any {
return arr[0]
}
const resultado = primerElemento(['hola', 'mundo'])
// resultado es any -- TypeScript no sabe que es string
resultado.toUpperCase() // No hay autocompletado ni verificacionCon genericos, resuelves ambos problemas:
function primerElemento<T>(arr: T[]): T {
return arr[0]
}
const texto = primerElemento(['hola', 'mundo'])
// texto es string -- TypeScript lo infiere automaticamente
const numero = primerElemento([1, 2, 3])
// numero es number
texto.toUpperCase() // Autocompletado completo
numero.toFixed(2) // Autocompletado completo<T> es el parametro de tipo. Cuando llamas a primerElemento(['hola', 'mundo']), TypeScript reemplaza T por string automaticamente.
Sintaxis basica: <T> explicado paso a paso
La convencion es usar letras mayusculas para los parametros de tipo:
| Letra | Uso comun |
|---|---|
T | Type -- tipo generico principal |
U | Segundo tipo generico |
K | Key -- claves de objetos |
V | Value -- valores |
E | Element -- elementos de colecciones |
R | Return -- tipo de retorno |
Puedes usar cualquier nombre, pero estas convenciones hacen que tu codigo sea reconocible para otros desarrolladores.
Forma explicita vs inferencia
// Forma explicita: tu defines el tipo
const texto = primerElemento<string>(['hola', 'mundo'])
// Forma con inferencia: TypeScript lo deduce del argumento
const texto = primerElemento(['hola', 'mundo'])
// Ambas producen el mismo resultado
// La inferencia es preferible cuando TypeScript puede resolverlo soloMultiples parametros de tipo
function crearPar<T, U>(primero: T, segundo: U): [T, U] {
return [primero, segundo]
}
const par = crearPar('edad', 30)
// par es [string, number]
const otroPar = crearPar(true, [1, 2, 3])
// otroPar es [boolean, number[]]Cuando usar genericos
Usa genericos cuando tengas una funcion o tipo que opera sobre la estructura de los datos sin importar su tipo concreto. Si la logica es la misma para strings, numbers u objetos, es candidata a ser generica.
Funciones genericas con ejemplos reales
Vamos con funciones que usarias en un proyecto real.
Wrapper para respuestas de API
// Tipo generico para respuestas de API
type ApiResponse<T> = {
data: T
status: number
message: string
timestamp: string
}
// Funcion generica para hacer fetch tipado
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`)
}
const json = await response.json()
return {
data: json as T,
status: response.status,
message: 'OK',
timestamp: new Date().toISOString(),
}
}
// Uso: TypeScript sabe exactamente que tipo tiene data
interface Usuario {
id: number
nombre: string
email: string
}
interface Producto {
id: number
titulo: string
precio: number
}
const usuarios = await fetchApi<Usuario[]>('/api/usuarios')
// usuarios.data es Usuario[]
usuarios.data[0].nombre // autocompletado completo
const productos = await fetchApi<Producto[]>('/api/productos')
// productos.data es Producto[]
productos.data[0].precio // autocompletado completoSi trabajas con fetch o async/await, este patron te ahorra repetir la misma logica de manejo de respuestas en cada llamada.
Funcion para buscar en arrays por propiedad
function buscarPor<T, K extends keyof T>(
items: T[],
propiedad: K,
valor: T[K]
): T | undefined {
return items.find(item => item[propiedad] === valor)
}
const usuarios: Usuario[] = [
{ id: 1, nombre: 'Ana', email: 'ana@mail.com' },
{ id: 2, nombre: 'Carlos', email: 'carlos@mail.com' },
]
// TypeScript sabe que propiedad debe ser 'id' | 'nombre' | 'email'
const ana = buscarPor(usuarios, 'nombre', 'Ana')
// ana es Usuario | undefined
// Error de compilacion: 'edad' no existe en Usuario
const error = buscarPor(usuarios, 'edad', 25)
// ^^^^
// Argument of type '"edad"' is not assignableFuncion de agrupacion
function agruparPor<T>(
items: T[],
clave: keyof T
): Record<string, T[]> {
return items.reduce((grupos, item) => {
const valor = String(item[clave])
if (!grupos[valor]) {
grupos[valor] = []
}
grupos[valor].push(item)
return grupos
}, {} as Record<string, T[]>)
}
interface Pedido {
id: number
estado: 'pendiente' | 'enviado' | 'entregado'
total: number
}
const pedidos: Pedido[] = [
{ id: 1, estado: 'pendiente', total: 100 },
{ id: 2, estado: 'enviado', total: 250 },
{ id: 3, estado: 'pendiente', total: 75 },
]
const porEstado = agruparPor(pedidos, 'estado')
// {
// pendiente: [{ id: 1, ... }, { id: 3, ... }],
// enviado: [{ id: 2, ... }]
// }Interfaces y tipos genericos
Los genericos funcionan igual en interfaces y type aliases.
Interface generica
// Interface generica para una coleccion paginada
interface PaginaResultados<T> {
items: T[]
total: number
pagina: number
porPagina: number
totalPaginas: number
}
// Uso con diferentes tipos
type PaginaUsuarios = PaginaResultados<Usuario>
type PaginaProductos = PaginaResultados<Producto>
// Funcion que retorna resultados paginados
async function obtenerPaginado<T>(
url: string,
pagina: number
): Promise<PaginaResultados<T>> {
const response = await fetch(`${url}?page=${pagina}`)
return response.json()
}
const paginaUsuarios = await obtenerPaginado<Usuario>('/api/usuarios', 1)
paginaUsuarios.items[0].nombre // string -- tipado correcto
paginaUsuarios.totalPaginas // numberType alias generico
// Estado de una operacion asincrona
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string }
// Uso
function procesarEstado<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle':
return 'Esperando...'
case 'loading':
return 'Cargando...'
case 'success':
// TypeScript sabe que state.data existe aqui
return `Datos: ${JSON.stringify(state.data)}`
case 'error':
// TypeScript sabe que state.error existe aqui
return `Error: ${state.error}`
}
}
const estadoUsuario: AsyncState<Usuario> = {
status: 'success',
data: { id: 1, nombre: 'Ana', email: 'ana@mail.com' },
}Este patron de discriminated union con genericos es comun en aplicaciones React para manejar estados de carga.
Interface generica con metodos
// Repositorio generico (patron comun en backends)
interface Repository<T> {
obtenerTodos(): Promise<T[]>
obtenerPorId(id: number): Promise<T | null>
crear(datos: Omit<T, 'id'>): Promise<T>
actualizar(id: number, datos: Partial<T>): Promise<T>
eliminar(id: number): Promise<boolean>
}
// Implementacion para usuarios
class UsuarioRepository implements Repository<Usuario> {
async obtenerTodos(): Promise<Usuario[]> {
const res = await fetch('/api/usuarios')
return res.json()
}
async obtenerPorId(id: number): Promise<Usuario | null> {
const res = await fetch(`/api/usuarios/${id}`)
if (!res.ok) return null
return res.json()
}
async crear(datos: Omit<Usuario, 'id'>): Promise<Usuario> {
const res = await fetch('/api/usuarios', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos),
})
return res.json()
}
async actualizar(id: number, datos: Partial<Usuario>): Promise<Usuario> {
const res = await fetch(`/api/usuarios/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos),
})
return res.json()
}
async eliminar(id: number): Promise<boolean> {
const res = await fetch(`/api/usuarios/${id}`, { method: 'DELETE' })
return res.ok
}
}Constraints con extends
Los constraints restringen que tipos puede aceptar un generico. Esto es fundamental cuando necesitas acceder a propiedades del tipo dentro de la funcion.
Constraint basico
// Sin constraint: error
function obtenerNombre<T>(obj: T): string {
return obj.nombre // Error: Property 'nombre' does not exist on type 'T'
}
// Con constraint: funciona
function obtenerNombre<T extends { nombre: string }>(obj: T): string {
return obj.nombre // OK: TypeScript sabe que T tiene nombre
}
obtenerNombre({ nombre: 'Ana', edad: 25 }) // OK
obtenerNombre({ nombre: 'Carlos', rol: 'admin' }) // OK
obtenerNombre({ edad: 25 }) // Error: falta nombrekeyof constraint
// Obtener el valor de una propiedad de forma segura
function obtenerPropiedad<T, K extends keyof T>(obj: T, clave: K): T[K] {
return obj[clave]
}
const usuario = { id: 1, nombre: 'Ana', email: 'ana@mail.com' }
const nombre = obtenerPropiedad(usuario, 'nombre')
// nombre es string
const id = obtenerPropiedad(usuario, 'id')
// id es number
obtenerPropiedad(usuario, 'telefono')
// Error: '"telefono"' is not assignable to '"id" | "nombre" | "email"'keyof en la practica
keyof T produce una union de todas las claves de T como strings literales. Es la base de muchos patrones avanzados y de los utility types nativos de TypeScript.
Constraint con interface
// Cualquier tipo que tenga un id
interface Identificable {
id: number
}
function encontrarPorId<T extends Identificable>(
items: T[],
id: number
): T | undefined {
return items.find(item => item.id === id)
}
// Funciona con cualquier tipo que tenga id
interface Producto {
id: number
titulo: string
precio: number
}
interface Categoria {
id: number
nombre: string
}
const productos: Producto[] = [
{ id: 1, titulo: 'Laptop', precio: 999 },
{ id: 2, titulo: 'Mouse', precio: 25 },
]
const categorias: Categoria[] = [
{ id: 1, nombre: 'Electronica' },
{ id: 2, nombre: 'Accesorios' },
]
encontrarPorId(productos, 1) // Producto | undefined
encontrarPorId(categorias, 1) // Categoria | undefinedMultiples constraints
// T debe tener id Y nombre
function mostrarResumen<T extends { id: number; nombre: string }>(
item: T
): string {
return `#${item.id}: ${item.nombre}`
}
// T debe extender dos interfaces
interface ConTimestamp {
createdAt: Date
updatedAt: Date
}
function ordenarPorFecha<T extends Identificable & ConTimestamp>(
items: T[]
): T[] {
return [...items].sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
}Genericos con valores por defecto
Igual que los parametros de funciones, los genericos pueden tener valores por defecto.
// Sin valor por defecto: siempre hay que especificarlo
interface Estado<T> {
datos: T
cargando: boolean
}
// Con valor por defecto: si no especificas T, usa unknown
interface Estado<T = unknown> {
datos: T
cargando: boolean
}
// Ambos son validos:
const estadoEspecifico: Estado<Usuario> = {
datos: { id: 1, nombre: 'Ana', email: 'ana@mail.com' },
cargando: false,
}
const estadoGeneral: Estado = {
datos: { lo: 'que sea' },
cargando: true,
}Ejemplo practico: componente de tabla
interface TablaConfig<T = Record<string, unknown>> {
columnas: (keyof T)[]
datos: T[]
ordenarPor?: keyof T
direccion?: 'asc' | 'desc'
}
// Con tipo especifico
const configUsuarios: TablaConfig<Usuario> = {
columnas: ['id', 'nombre', 'email'],
datos: usuarios,
ordenarPor: 'nombre',
direccion: 'asc',
}
// Error si pones una columna que no existe
const configMala: TablaConfig<Usuario> = {
columnas: ['id', 'telefono'], // Error: 'telefono' no es keyof Usuario
datos: usuarios,
}Valor por defecto con constraint
// T por defecto es string, pero puede ser cualquier cosa que extienda string | number
type Identificador<T extends string | number = string> = {
valor: T
tipo: T extends string ? 'texto' : 'numerico'
}
const idTexto: Identificador = { valor: 'abc-123', tipo: 'texto' }
const idNumero: Identificador<number> = { valor: 42, tipo: 'numerico' }Utility Types que usan genericos
TypeScript incluye utility types que estan construidos internamente con genericos. Entender como funcionan te ayuda a crear los tuyos.
Partial<T>
Hace todas las propiedades opcionales:
interface Usuario {
id: number
nombre: string
email: string
rol: 'admin' | 'usuario'
}
// Partial<Usuario> es equivalente a:
// {
// id?: number
// nombre?: string
// email?: string
// rol?: 'admin' | 'usuario'
// }
// Caso de uso: actualizacion parcial
function actualizarUsuario(
id: number,
cambios: Partial<Usuario>
): Promise<Usuario> {
return fetch(`/api/usuarios/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cambios),
}).then(res => res.json())
}
// Puedes enviar solo los campos que quieras cambiar
actualizarUsuario(1, { nombre: 'Ana Maria' })
actualizarUsuario(1, { rol: 'admin' })
actualizarUsuario(1, { nombre: 'Ana', email: 'ana@nueva.com' })Como se implementa internamente:
// La implementacion real de Partial en TypeScript
type MiPartial<T> = {
[K in keyof T]?: T[K]
}Pick<T, K>
Selecciona propiedades especificas:
// Solo id y nombre del usuario
type UsuarioResumen = Pick<Usuario, 'id' | 'nombre'>
// { id: number; nombre: string }
// Caso de uso: respuesta de lista (sin datos sensibles)
async function listarUsuarios(): Promise<Pick<Usuario, 'id' | 'nombre'>[]> {
const res = await fetch('/api/usuarios?fields=id,nombre')
return res.json()
}
const lista = await listarUsuarios()
lista[0].id // OK
lista[0].nombre // OK
lista[0].email // Error: no existe en el tipoOmit<T, K>
Excluye propiedades (lo opuesto a Pick):
// Todo menos el id (para crear nuevos registros)
type NuevoUsuario = Omit<Usuario, 'id'>
// { nombre: string; email: string; rol: 'admin' | 'usuario' }
function crearUsuario(datos: Omit<Usuario, 'id'>): Promise<Usuario> {
return fetch('/api/usuarios', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos),
}).then(res => res.json())
}
// No necesitas (ni puedes) pasar id
crearUsuario({
nombre: 'Carlos',
email: 'carlos@mail.com',
rol: 'usuario',
})Record<K, V>
Crea un tipo de objeto con claves y valores tipados:
// Mapa de errores por campo
type CamposFormulario = 'nombre' | 'email' | 'password'
type Errores = Record<CamposFormulario, string[]>
const errores: Errores = {
nombre: ['Nombre requerido'],
email: ['Email invalido', 'Email ya registrado'],
password: ['Minimo 8 caracteres'],
}
// Mapa de configuracion
type Entorno = 'development' | 'staging' | 'production'
const config: Record<Entorno, { apiUrl: string; debug: boolean }> = {
development: { apiUrl: 'http://localhost:3000', debug: true },
staging: { apiUrl: 'https://staging.api.com', debug: true },
production: { apiUrl: 'https://api.com', debug: false },
}Combinando utility types
// Crear un tipo para editar: todo opcional excepto id
type EditarUsuario = Pick<Usuario, 'id'> & Partial<Omit<Usuario, 'id'>>
// { id: number; nombre?: string; email?: string; rol?: 'admin' | 'usuario' }
// Tipo para formulario de registro: todo requerido menos id
type FormRegistro = Required<Omit<Usuario, 'id'>>
// { nombre: string; email: string; rol: 'admin' | 'usuario' }
// Tipo readonly (inmutable)
type UsuarioReadonly = Readonly<Usuario>
const usuario: UsuarioReadonly = {
id: 1,
nombre: 'Ana',
email: 'ana@mail.com',
rol: 'admin',
}
usuario.nombre = 'Otro' // Error: Cannot assign to 'nombre' because it is a read-only propertyTip: combina utility types
La fuerza real de estos tipos esta en combinarlos. Pick, Omit, Partial y Required cubren la mayoria de las transformaciones de tipos que necesitas en el dia a dia.
Patrones avanzados: genericos en componentes React
Los genericos son especialmente utiles en componentes React que necesitan ser reutilizables con diferentes tipos de datos. Si ya manejas los hooks y ciclo de vida de React, esto te va a resultar natural.
Componente de lista generica
interface ListaProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
keyExtractor: (item: T) => string | number
emptyMessage?: string
}
function Lista<T>({ items, renderItem, keyExtractor, emptyMessage }: ListaProps<T>) {
if (items.length === 0) {
return <p>{emptyMessage ?? 'No hay elementos'}</p>
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
)
}
// Uso con usuarios
<Lista<Usuario>
items={usuarios}
keyExtractor={(u) => u.id}
renderItem={(usuario) => (
<div>
<strong>{usuario.nombre}</strong>
<span>{usuario.email}</span>
</div>
)}
/>
// Uso con productos -- mismo componente, diferente tipo
<Lista<Producto>
items={productos}
keyExtractor={(p) => p.id}
renderItem={(producto) => (
<div>
<strong>{producto.titulo}</strong>
<span>${producto.precio}</span>
</div>
)}
/>Hook generico para fetch de datos
import { useState, useEffect } from 'react'
interface UseFetchResult<T> {
data: T | null
error: string | null
loading: boolean
refetch: () => void
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const fetchData = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Error: ${response.status}`)
}
const json: T = await response.json()
setData(json)
} catch (err) {
setError(err instanceof Error ? err.message : 'Error desconocido')
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchData()
}, [url])
return { data, error, loading, refetch: fetchData }
}
// Uso en un componente
function PaginaUsuarios() {
const { data, loading, error } = useFetch<Usuario[]>('/api/usuarios')
if (loading) return <p>Cargando...</p>
if (error) return <p>Error: {error}</p>
if (!data) return null
return (
<ul>
{data.map(usuario => (
<li key={usuario.id}>{usuario.nombre}</li>
))}
</ul>
)
}Select generico con tipado completo
interface SelectProps<T> {
options: T[]
value: T | null
onChange: (selected: T) => void
getLabel: (option: T) => string
getValue: (option: T) => string | number
placeholder?: string
}
function Select<T>({
options,
value,
onChange,
getLabel,
getValue,
placeholder = 'Selecciona una opcion',
}: SelectProps<T>) {
return (
<select
value={value ? String(getValue(value)) : ''}
onChange={(e) => {
const selected = options.find(
(opt) => String(getValue(opt)) === e.target.value
)
if (selected) onChange(selected)
}}
>
<option value="" disabled>
{placeholder}
</option>
{options.map((option) => (
<option key={getValue(option)} value={getValue(option)}>
{getLabel(option)}
</option>
))}
</select>
)
}
// Uso
interface Pais {
codigo: string
nombre: string
poblacion: number
}
const paises: Pais[] = [
{ codigo: 'MX', nombre: 'Mexico', poblacion: 128900000 },
{ codigo: 'CO', nombre: 'Colombia', poblacion: 51870000 },
{ codigo: 'AR', nombre: 'Argentina', poblacion: 45380000 },
]
<Select<Pais>
options={paises}
value={paisSeleccionado}
onChange={(pais) => {
// pais es Pais, no any
console.log(pais.codigo, pais.poblacion)
}}
getLabel={(p) => p.nombre}
getValue={(p) => p.codigo}
/>Genericos en formularios con Zod
Si usas Zod para validar schemas, puedes combinar z.infer con genericos para tener formularios completamente tipados:
import { z } from 'zod'
// Schema reutilizable con genericos
function crearFormulario<T extends z.ZodObject<any>>(schema: T) {
type FormData = z.infer<T>
return {
validar: (datos: unknown): FormData => schema.parse(datos),
validarSeguro: (datos: unknown) => schema.safeParse(datos),
valoresIniciales: () => {
const shape = schema.shape
const inicial: Record<string, unknown> = {}
for (const key of Object.keys(shape)) {
inicial[key] = ''
}
return inicial as FormData
},
}
}
// Uso
const registroSchema = z.object({
nombre: z.string().min(2),
email: z.string().email(),
edad: z.number().min(18),
})
const formulario = crearFormulario(registroSchema)
// TypeScript sabe el tipo exacto
const datos = formulario.validar({
nombre: 'Ana',
email: 'ana@mail.com',
edad: 25,
})
// datos es { nombre: string; email: string; edad: number }Crear tus propios utility types
Una vez que entiendes los genericos, puedes crear utility types personalizados para tu proyecto.
NonNullableProps: eliminar null de propiedades
type NonNullableProps<T> = {
[K in keyof T]: NonNullable<T[K]>
}
interface FormularioContacto {
nombre: string | null
email: string | null
mensaje: string | null
}
type FormularioCompleto = NonNullableProps<FormularioContacto>
// { nombre: string; email: string; mensaje: string }PickByType: seleccionar propiedades por tipo
type PickByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K]
}
interface Producto {
id: number
titulo: string
descripcion: string
precio: number
enStock: boolean
}
type CamposTexto = PickByType<Producto, string>
// { titulo: string; descripcion: string }
type CamposNumericos = PickByType<Producto, number>
// { id: number; precio: number }DeepPartial: Partial recursivo
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
interface Configuracion {
base: {
apiUrl: string
timeout: number
}
auth: {
token: string
refreshToken: string
opciones: {
recordar: boolean
expiracion: number
}
}
}
// Con Partial normal: solo el primer nivel es opcional
type ConfigParcial = Partial<Configuracion>
// base y auth son opcionales, pero sus propiedades internas siguen requeridas
// Con DeepPartial: todo es opcional en todos los niveles
type ConfigDeepParcial = DeepPartial<Configuracion>
const config: ConfigDeepParcial = {
auth: {
opciones: {
recordar: true,
// expiracion es opcional
},
// token y refreshToken son opcionales
},
// base es opcional
}PathsOf: obtener todas las rutas de un objeto
type PathsOf<T, Prefix extends string = ''> = {
[K in keyof T & string]: T[K] extends object
? PathsOf<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`
}[keyof T & string]
// Util para acceder a valores anidados de forma segura
type ConfigPaths = PathsOf<Configuracion>
// "base.apiUrl" | "base.timeout" | "auth.token" | "auth.refreshToken" | ...Cuidado con la complejidad
Los utility types recursivos son poderosos pero pueden ralentizar el compilador de TypeScript en proyectos grandes. Usa DeepPartial y tipos recursivos solo cuando realmente los necesites.
Genericos con funciones de tipo overload
A veces necesitas que una funcion retorne tipos diferentes segun los argumentos:
// Overloads + genericos
function parsear<T extends 'string' | 'number' | 'boolean'>(
valor: string,
tipo: T
): T extends 'string'
? string
: T extends 'number'
? number
: boolean {
switch (tipo) {
case 'string':
return valor as any
case 'number':
return Number(valor) as any
case 'boolean':
return (valor === 'true') as any
default:
throw new Error(`Tipo no soportado: ${tipo}`)
}
}
const texto = parsear('hola', 'string') // string
const numero = parsear('42', 'number') // number
const bool = parsear('true', 'boolean') // booleanGenericos en clases
// Cache generico con TTL (time to live)
class Cache<T> {
private store = new Map<string, { value: T; expiresAt: number }>()
constructor(private ttlMs: number = 60000) {}
set(key: string, value: T): void {
this.store.set(key, {
value,
expiresAt: Date.now() + this.ttlMs,
})
}
get(key: string): T | null {
const entry = this.store.get(key)
if (!entry) return null
if (Date.now() > entry.expiresAt) {
this.store.delete(key)
return null
}
return entry.value
}
clear(): void {
this.store.clear()
}
}
// Cache de usuarios (5 minutos)
const cacheUsuarios = new Cache<Usuario>(5 * 60 * 1000)
cacheUsuarios.set('user-1', { id: 1, nombre: 'Ana', email: 'ana@mail.com' })
const usuario = cacheUsuarios.get('user-1')
// usuario es Usuario | null
// Cache de productos (1 minuto)
const cacheProductos = new Cache<Producto>(60000)Errores comunes y como resolverlos
Error 1: "Type 'T' is not assignable to..."
// MAL: T puede ser cualquier cosa
function duplicar<T>(valor: T): T {
return valor * 2
// ^^^^^^^^^ Error: T no se puede multiplicar
}
// BIEN: restringir T a number
function duplicar<T extends number>(valor: T): number {
return valor * 2
}Error 2: No puedes crear instancias de un tipo generico
// MAL: no puedes hacer new T()
function crearInstancia<T>(): T {
return new T()
// ^^^^^ Error: 'T' only refers to a type
}
// BIEN: pasar un constructor como argumento
function crearInstancia<T>(Constructor: new () => T): T {
return new Constructor()
}
class MiClase {
nombre = 'instancia'
}
const obj = crearInstancia(MiClase) // MiClaseError 3: Genericos en arrow functions de JSX
// MAL: el <T> se confunde con JSX
const identidad = <T>(valor: T): T => valor
// ^ Error: JSX element 'T' has no corresponding closing tag
// BIEN: agregar extends para desambiguar
const identidad = <T extends unknown>(valor: T): T => valor
// BIEN: usar una interface constraint
const identidad = <T,>(valor: T): T => valor
// La coma despues de T le dice a TypeScript que es un generico, no JSXError frecuente en React
En archivos .tsx, <T> se interpreta como JSX. Usa <T,> o <T extends unknown> para que TypeScript lo trate como un parametro de tipo generico.
Error 4: Inferencia incorrecta con objetos literales
// MAL: TypeScript infiere un tipo demasiado amplio
function crearConfig<T>(config: T): T {
return config
}
const config = crearConfig({
url: 'https://api.com',
timeout: 5000,
})
// config es { url: string; timeout: number }
// Pierde el literal 'https://api.com'
// BIEN: usar const assertion
const config = crearConfig({
url: 'https://api.com',
timeout: 5000,
} as const)
// config es { readonly url: 'https://api.com'; readonly timeout: 5000 }Error 5: Demasiados genericos
// MAL: dificil de leer y usar
function procesar<T, U, V, W, X>(
datos: T,
transformar: (d: T) => U,
filtrar: (d: U) => V,
mapear: (d: V) => W,
reducir: (d: W) => X
): X {
return reducir(mapear(filtrar(transformar(datos))))
}
// BIEN: dividir en funciones mas pequenas
function transformar<T, U>(datos: T, fn: (d: T) => U): U {
return fn(datos)
}Regla practica
Si tu funcion tiene mas de 3 parametros de tipo generico, probablemente necesita ser dividida en funciones mas pequenas. El codigo generico debe simplificar, no complicar.
Patrones utiles del mundo real
Event emitter tipado
type EventMap = Record<string, unknown>
class TypedEmitter<Events extends EventMap> {
private listeners = new Map<keyof Events, Set<(data: any) => void>>()
on<K extends keyof Events>(
evento: K,
callback: (data: Events[K]) => void
): void {
if (!this.listeners.has(evento)) {
this.listeners.set(evento, new Set())
}
this.listeners.get(evento)!.add(callback)
}
emit<K extends keyof Events>(evento: K, data: Events[K]): void {
this.listeners.get(evento)?.forEach(cb => cb(data))
}
off<K extends keyof Events>(
evento: K,
callback: (data: Events[K]) => void
): void {
this.listeners.get(evento)?.delete(callback)
}
}
// Definir los eventos de tu app
interface AppEvents {
'usuario:login': { id: number; nombre: string }
'usuario:logout': { id: number }
'producto:comprado': { productoId: number; cantidad: number }
'error': { mensaje: string; codigo: number }
}
const emitter = new TypedEmitter<AppEvents>()
// Autocompletado de eventos y datos
emitter.on('usuario:login', (data) => {
console.log(data.nombre) // TypeScript sabe que existe
})
emitter.emit('producto:comprado', {
productoId: 1,
cantidad: 3,
})
// Error: falta 'cantidad'
emitter.emit('producto:comprado', { productoId: 1 })Builder pattern tipado
class QueryBuilder<T> {
private conditions: string[] = []
private selectedFields: (keyof T)[] = []
private orderField?: keyof T
private limitValue?: number
select(...campos: (keyof T)[]): this {
this.selectedFields = campos
return this
}
where(campo: keyof T, operador: '=' | '>' | '<' | '!=', valor: T[keyof T]): this {
this.conditions.push(`${String(campo)} ${operador} '${valor}'`)
return this
}
orderBy(campo: keyof T): this {
this.orderField = campo
return this
}
limit(n: number): this {
this.limitValue = n
return this
}
build(): string {
const campos = this.selectedFields.length > 0
? this.selectedFields.join(', ')
: '*'
let query = `SELECT ${campos} FROM tabla`
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(' AND ')}`
}
if (this.orderField) {
query += ` ORDER BY ${String(this.orderField)}`
}
if (this.limitValue) {
query += ` LIMIT ${this.limitValue}`
}
return query
}
}
// Uso tipado
const query = new QueryBuilder<Usuario>()
.select('nombre', 'email')
.where('rol', '=', 'admin')
.orderBy('nombre')
.limit(10)
.build()
// Error en compilacion:
new QueryBuilder<Usuario>()
.select('telefono') // Error: 'telefono' no existe en Usuario
.where('edad', '>', 18) // Error: 'edad' no existe en UsuarioReferencia rapida
| Concepto | Sintaxis | Ejemplo |
|---|---|---|
| Funcion generica | function fn<T>(arg: T): T | primerElemento<string>(['a']) |
| Interface generica | interface I<T> { prop: T } | Estado<Usuario> |
| Constraint | <T extends Tipo> | <T extends { id: number }> |
| Valor por defecto | <T = TipoDefault> | <T = unknown> |
| keyof | K extends keyof T | Acceso seguro a propiedades |
| Partial | Partial<T> | Propiedades opcionales |
| Pick | Pick<T, 'a' | 'b'> | Seleccionar propiedades |
| Omit | Omit<T, 'a'> | Excluir propiedades |
| Record | Record<K, V> | Objeto tipado |
| Required | Required<T> | Todo obligatorio |
| Readonly | Readonly<T> | Todo inmutable |
Recursos adicionales
- TypeScript Handbook: Generics -- la documentacion oficial es la referencia mas completa.
- TypeScript Playground -- experimenta con genericos directamente en el navegador sin instalar nada.
Preguntas frecuentes
Cuando debo usar genericos y cuando tipos concretos?
Usa genericos cuando la logica de tu funcion o tipo no depende del tipo concreto. Si una funcion hace lo mismo con strings, numbers u objetos, es candidata. Si la logica es especifica para un tipo (calcular un descuento, formatear una fecha), usa tipos concretos.
Los genericos afectan el rendimiento en runtime?
No. Los genericos son un concepto exclusivo de TypeScript que desaparece completamente cuando el codigo se compila a JavaScript. No hay ningun overhead en runtime -- son solo instrucciones para el compilador.
Puedo usar genericos con Zod?
Si. Puedes usar z.infer<typeof schema> para extraer el tipo de un schema de Zod. Esto combina la validacion en runtime de Zod con el tipado estatico de TypeScript. Es uno de los patrones mas potentes de TypeScript moderno.
Cual es la diferencia entre <T> y <T extends unknown>?
En la practica, son equivalentes. <T extends unknown> se usa en archivos .tsx para que TypeScript no confunda el generico con una etiqueta JSX. Tambien puedes usar <T,> (con coma) como alternativa mas corta.
Cuantos parametros de tipo generico es razonable tener?
Como regla general, no mas de 3. Si necesitas mas, tu funcion probablemente esta haciendo demasiado y deberia dividirse. Las funciones con muchos genericos son dificiles de leer, dificiles de usar, y dificiles de mantener.
Preguntas frecuentes
Que son los tipos genericos en TypeScript?
Los tipos genericos en TypeScript son una forma de crear funciones, interfaces y clases que trabajan con multiples tipos sin perder el tipado estatico. En lugar de usar any, defines un parametro de tipo como <T> que se resuelve al tipo concreto cuando usas la funcion o interfaz. Esto te da reutilizacion y seguridad de tipos al mismo tiempo.
Cual es la diferencia entre usar genericos y usar any en TypeScript?
Con any pierdes toda la informacion de tipo y el compilador no puede ayudarte a detectar errores. Con genericos, TypeScript infiere y mantiene el tipo concreto a lo largo de toda la operacion. Si pasas un string a una funcion generica, TypeScript sabe que el resultado tambien es string, algo imposible con any.
Como usar genericos en componentes de React con TypeScript?
Defines el componente con un parametro de tipo generico, por ejemplo function Lista<T>(props: ListaProps<T>). Esto permite que el componente acepte datos de cualquier tipo manteniendo el tipado correcto en las props, callbacks y renders. Es el patron que usan librerias como React Hook Form y TanStack Table.
Que son los constraints con extends en genericos de TypeScript?
Los constraints limitan que tipos puede aceptar un generico. Por ejemplo, <T extends { id: number }> significa que T debe ser un objeto que tenga al menos una propiedad id de tipo number. Esto te permite acceder a propiedades de T dentro de la funcion con seguridad de tipos.
Cuales son los utility types mas usados en TypeScript?
Los mas usados son Partial<T> que hace todas las propiedades opcionales, Pick<T, K> que selecciona propiedades especificas, Omit<T, K> que excluye propiedades, Record<K, V> que crea un tipo de objeto con claves y valores tipados, y Required<T> que hace todas las propiedades obligatorias. Todos estan construidos con genericos internamente.
Articulos relacionados
Zod Avanzado: Discriminated Unions, Transforms y Pipes
Patrones avanzados de Zod: discriminated unions, transforms, pipes, preprocess, y como validar datos complejos en TypeScript con schemas reutilizables.
tRPC + Next.js: APIs Type-Safe sin REST
Implementa tRPC en Next.js para APIs 100% type-safe. Sin schemas de API, sin fetch manual, sin types duplicados. End-to-end type safety con TypeScript.
Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificacion de firmas, tipado y manejo de errores.