Mucha gente usa TypeScript sin haber escrito nunca un generic. El código compila, funciona, y parece suficiente. 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

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. Escribe una vez, tipa 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

A veces el generic necesita una garantía. 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

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

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

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

TypeScript ya viene con utility types listos. 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 acepta callbacks

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 tipo. 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.