tutoriales·14 min de lectura

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:

typescript
// 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 verificacion

Con genericos, resuelves ambos problemas:

typescript
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:

LetraUso comun
TType -- tipo generico principal
USegundo tipo generico
KKey -- claves de objetos
VValue -- valores
EElement -- elementos de colecciones
RReturn -- tipo de retorno

Puedes usar cualquier nombre, pero estas convenciones hacen que tu codigo sea reconocible para otros desarrolladores.

Forma explicita vs inferencia

typescript
// 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 solo

Multiples parametros de tipo

typescript
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

typescript
// 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 completo

Si 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

typescript
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 assignable

Funcion de agrupacion

typescript
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

typescript
// 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    // number

Type alias generico

typescript
// 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

typescript
// 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

typescript
// 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 nombre

keyof constraint

typescript
// 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

typescript
// 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 | undefined

Multiples constraints

typescript
// 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.

typescript
// 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

typescript
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

typescript
// 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:

typescript
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:

typescript
// La implementacion real de Partial en TypeScript
type MiPartial<T> = {
  [K in keyof T]?: T[K]
}

Pick<T, K>

Selecciona propiedades especificas:

typescript
// 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 tipo

Omit<T, K>

Excluye propiedades (lo opuesto a Pick):

typescript
// 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:

typescript
// 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

typescript
// 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 property
Tip: 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

tsx
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

tsx
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

tsx
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:

tsx
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

typescript
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

typescript
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

typescript
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

typescript
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:

typescript
// 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')    // boolean

Genericos en clases

typescript
// 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..."

typescript
// 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

typescript
// 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) // MiClase

Error 3: Genericos en arrow functions de JSX

tsx
// 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 JSX
Error 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

typescript
// 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

typescript
// 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

typescript
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

typescript
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 Usuario

Referencia rapida

ConceptoSintaxisEjemplo
Funcion genericafunction fn<T>(arg: T): TprimerElemento<string>(['a'])
Interface genericainterface I<T> { prop: T }Estado<Usuario>
Constraint<T extends Tipo><T extends { id: number }>
Valor por defecto<T = TipoDefault><T = unknown>
keyofK extends keyof TAcceso seguro a propiedades
PartialPartial<T>Propiedades opcionales
PickPick<T, 'a' | 'b'>Seleccionar propiedades
OmitOmit<T, 'a'>Excluir propiedades
RecordRecord<K, V>Objeto tipado
RequiredRequired<T>Todo obligatorio
ReadonlyReadonly<T>Todo inmutable

Recursos adicionales

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.

#typescript#generics#tipos#react

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.