Tipos genéricos básicos

Intermedio
TypeScript
TypeScript
Actualizado: 18/04/2026

Sintaxis genérica y el patrón identity

Los tipos genéricos permiten crear componentes reutilizables que trabajan con distintos tipos de datos sin perder la seguridad de tipos. La sintaxis genérica se basa en los corchetes angulares <> y un parámetro de tipo, convencionalmente representado con la letra T.

Flujo de parámetros de tipo genéricos

Sin genéricos, una función que devuelve el mismo valor que recibe necesita un tipo concreto o recurrir a any:

function identityNumber(arg: number): number {
    return arg
}

function identityAny(arg: any): any {
    return arg
}

La versión con any acepta cualquier tipo, pero pierde la información sobre qué tipo se pasó. Los genéricos resuelven este problema capturando el tipo del argumento en un parámetro de tipo:

function identity<T>(arg: T): T {
    return arg
}

El parámetro de tipo T actúa como una variable que opera sobre tipos en lugar de valores. Captura el tipo proporcionado y lo utiliza tanto en el parámetro como en el retorno.

El resultado es una función que acepta cualquier tipo y devuelve exactamente ese mismo tipo, manteniendo la trazabilidad completa del tipo a lo largo de la operación.

Inferencia de parámetros de tipo

TypeScript puede inferir automáticamente el tipo genérico a partir del argumento proporcionado. Existen dos formas de invocar una función genérica:

function identity<T>(arg: T): T {
    return arg
}

// Forma explícita: se indica el tipo entre <>
let resultado1 = identity<string>("hola")

// Forma implícita: TypeScript infiere T como string
let resultado2 = identity("hola")

// TypeScript infiere T como number
let resultado3 = identity(42)

// TypeScript infiere T como boolean
let resultado4 = identity(true)

La inferencia de tipos es la forma preferida porque produce código más limpio. Solo es necesario especificar el tipo explícitamente cuando TypeScript no puede inferirlo correctamente, algo que ocurre en escenarios más complejos:

function crearPar<T, U>(a: T, b: U): [T, U] {
    return [a, b]
}

// Inferencia correcta: [string, number]
let par1 = crearPar("edad", 30)

// Especificación explícita cuando se necesita un tipo más amplio
let par2 = crearPar<string, string | number>("clave", 42)

La inferencia de tipos genéricos funciona analizando los argumentos pasados a la función. Cuando el compilador puede determinar el tipo sin ambigüedad, la especificación explícita es innecesaria.

Funciones genéricas

Las funciones genéricas son la aplicación más directa de los genéricos. Permiten escribir operaciones que funcionan con cualquier tipo manteniendo la coherencia de tipos en entrada y salida.

Funciones que operan sobre arrays

Los genéricos son especialmente útiles para funciones que trabajan con colecciones:

function primerElemento<T>(array: T[]): T | undefined {
    return array.length > 0 ? array[0] : undefined
}

const num = primerElemento([10, 20, 30])        // number | undefined
const texto = primerElemento(["a", "b", "c"])   // string | undefined
const vacío = primerElemento([])                 // undefined
function invertirArray<T>(items: T[]): T[] {
    return [...items].reverse()
}

const números = invertirArray([1, 2, 3])           // number[]
const nombres = invertirArray(["Ana", "Luis"])     // string[]

Funciones con múltiples parámetros de tipo

Cuando una función necesita relacionar tipos distintos, se pueden declarar varios parámetros de tipo:

function transformar<T, U>(items: T[], fn: (item: T) => U): U[] {
    return items.map(fn)
}

const longitudes = transformar(["hola", "mundo"], s => s.length)    // number[]
const textos = transformar([1, 2, 3], n => n.toString())            // string[]
function combinar<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 }
}

const resultado = combinar(
    { nombre: "Elena" },
    { edad: 28 }
)
// tipo: { nombre: string } & { edad: number }
console.log(resultado.nombre)  // "Elena"
console.log(resultado.edad)    // 28

Funciones flecha genéricas

Las arrow functions también admiten parámetros de tipo. En archivos .tsx puede ser necesario añadir una coma tras el parámetro para evitar ambigüedad con etiquetas JSX:

const envolver = <T>(valor: T): T[] => {
    return [valor]
}

const nums = envolver(5)        // number[]
const strs = envolver("hola")   // string[]

// En archivos .tsx se usa la coma:
const envolverTsx = <T,>(valor: T): T[] => [valor]

Funciones de orden superior genéricas

Los genéricos permiten crear funciones que aceptan o devuelven otras funciones de forma tipada:

function ejecutar<T, U>(dato: T, operación: (x: T) => U): U {
    return operación(dato)
}

const longitud = ejecutar("TypeScript", x => x.length)   // number
const doble = ejecutar(5, x => x * 2)                     // number
function componer<A, B, C>(
    f: (b: B) => C,
    g: (a: A) => B
): (a: A) => C {
    return (a) => f(g(a))
}

const parsearYDuplicar = componer(
    (n: number) => n * 2,
    (s: string) => parseInt(s, 10)
)

console.log(parsearYDuplicar("15"))  // 30

Interfaces genéricas

Las interfaces genéricas definen contratos flexibles que pueden adaptarse a distintos tipos de datos. El parámetro de tipo se declara junto al nombre de la interfaz:

interface Caja<T> {
    contenido: T
}

let cajaTexto: Caja<string> = { contenido: "TypeScript" }
let cajaNumero: Caja<number> = { contenido: 42 }

Las interfaces genéricas pueden definir métodos que utilicen el parámetro de tipo:

interface Coleccion<T> {
    elementos: T[]
    agregar(item: T): void
    obtener(indice: number): T | undefined
    filtrar(predicado: (item: T) => boolean): T[]
}

function crearColeccion<T>(): Coleccion<T> {
    const elementos: T[] = []
    return {
        elementos,
        agregar(item: T) {
            elementos.push(item)
        },
        obtener(indice: number) {
            return elementos[indice]
        },
        filtrar(predicado: (item: T) => boolean) {
            return elementos.filter(predicado)
        }
    }
}

const números = crearColeccion<number>()
números.agregar(10)
números.agregar(20)
números.agregar(30)

const mayoresA15 = números.filtrar(n => n > 15)  // [20, 30]

Una interfaz genérica con múltiples parámetros permite modelar relaciones entre tipos:

interface Par<K, V> {
    clave: K
    valor: V
}

const entrada: Par<string, number> = { clave: "edad", valor: 30 }
const config: Par<string, boolean> = { clave: "activo", valor: true }
interface Resultado<T, E> {
    exito: boolean
    datos?: T
    error?: E
}

function dividir(a: number, b: number): Resultado<number, string> {
    if (b === 0) {
        return { exito: false, error: "Division por cero" }
    }
    return { exito: true, datos: a / b }
}

const r = dividir(10, 2)
if (r.exito) {
    console.log(r.datos)  // 5
}

Alias de tipo genéricos

Los alias de tipo (type) también aceptan parámetros genéricos, lo que permite crear tipos reutilizables con una sintaxis concisa:

type Respuesta<T> = {
    datos: T
    estado: number
    mensaje: string
}

const respuestaTexto: Respuesta<string> = {
    datos: "OK",
    estado: 200,
    mensaje: "Operación exitosa"
}

interface Usuario {
    id: number
    nombre: string
}

const respuestaUsuario: Respuesta<Usuario> = {
    datos: { id: 1, nombre: "Ana" },
    estado: 200,
    mensaje: "Usuario encontrado"
}

Los alias genéricos son especialmente expresivos con tipos union discriminados:

type ResultadoOperacion<T, E = string> =
    | { tipo: "exito"; valor: T }
    | { tipo: "error"; error: E }

function parsearJSON<T>(texto: string): ResultadoOperacion<T> {
    try {
        const datos = JSON.parse(texto) as T
        return { tipo: "exito", valor: datos }
    } catch (e) {
        return { tipo: "error", error: "JSON inválido" }
    }
}

const resultado = parsearJSON<{ nombre: string }>('{"nombre": "Luis"}')

if (resultado.tipo === "exito") {
    console.log(resultado.valor.nombre)  // "Luis"
}

También se pueden crear alias genéricos para funciones:

type Transformador<T, U> = (entrada: T) => U

const aTexto: Transformador<number, string> = n => n.toString()
const aNumero: Transformador<string, number> = s => parseInt(s, 10)

console.log(aTexto(42))       // "42"
console.log(aNumero("100"))   // 100

Parámetros de tipo por defecto

TypeScript permite asignar valores por defecto a los parámetros de tipo, de forma similar a los valores por defecto en parámetros de funciones:

interface RespuestaAPI<T = any> {
    datos: T
    estado: number
}

// Sin especificar tipo: T es any
const genérico: RespuestaAPI = {
    datos: { lo_que_sea: true },
    estado: 200
}

// Especificando tipo concreto
const tipado: RespuestaAPI<string[]> = {
    datos: ["a", "b", "c"],
    estado: 200
}

Los valores por defecto son especialmente útiles cuando un tipo genérico tiene múltiples parámetros y algunos tienen un valor habitual:

type Evento<TDatos = void, TMeta = Record<string, unknown>> = {
    tipo: string
    datos: TDatos
    meta: TMeta
    timestamp: number
}

// Solo especificamos el primer parámetro
const eventoSimple: Evento = {
    tipo: "click",
    datos: undefined,
    meta: {},
    timestamp: Date.now()
}

// Especificamos ambos parámetros
const eventoCompleto: Evento<{ x: number; y: number }, { fuente: string }> = {
    tipo: "click",
    datos: { x: 100, y: 200 },
    meta: { fuente: "raton" },
    timestamp: Date.now()
}

Los parámetros con valor por defecto deben aparecer después de los parámetros sin valor por defecto, igual que en las funciones regulares.

type Cache<TValor, TClave = string> = {
    obtener(clave: TClave): TValor | undefined
    establecer(clave: TClave, valor: TValor): void
}

// TClave usa el valor por defecto string
const cacheNumeros: Cache<number> = {
    obtener(clave: string) { return undefined },
    establecer(clave: string, valor: number) { }
}

// TClave se específica como number
const cacheIndexado: Cache<string, number> = {
    obtener(clave: number) { return undefined },
    establecer(clave: number, valor: string) { }
}

Trabajar con arrays de forma genérica

Los arrays son una de las estructuras donde los genéricos brillan con mayor claridad. TypeScript proporciona el tipo Array<T> como alias del tipo T[], y ambas notaciones son equivalentes:

let numeros1: Array<number> = [1, 2, 3]
let numeros2: number[] = [1, 2, 3]  // equivalente

Crear funciones genéricas que operen sobre arrays permite reutilizar lógica para cualquier tipo de elemento:

function ultimoElemento<T>(arr: T[]): T | undefined {
    return arr[arr.length - 1]
}

function agruparDe<T>(arr: T[], tamano: number): T[][] {
    const grupos: T[][] = []
    for (let i = 0; i < arr.length; i += tamano) {
        grupos.push(arr.slice(i, i + tamano))
    }
    return grupos
}

console.log(ultimoElemento([10, 20, 30]))        // 30
console.log(ultimoElemento(["a", "b"]))           // "b"

console.log(agruparDe([1, 2, 3, 4, 5], 2))       // [[1, 2], [3, 4], [5]]
console.log(agruparDe(["a", "b", "c", "d"], 3))   // [["a", "b", "c"], ["d"]]

Las funciones genéricas sobre arrays pueden combinar búsqueda, filtrado y transformación:

function buscar<T>(
    arr: T[],
    predicado: (item: T) => boolean
): T | undefined {
    for (const item of arr) {
        if (predicado(item)) return item
    }
    return undefined
}

interface Producto {
    id: number
    nombre: string
    precio: number
}

const productos: Producto[] = [
    { id: 1, nombre: "Teclado", precio: 50 },
    { id: 2, nombre: "Raton", precio: 25 },
    { id: 3, nombre: "Monitor", precio: 300 }
]

const caro = buscar(productos, p => p.precio > 100)
console.log(caro?.nombre)  // "Monitor"
function eliminarDuplicados<T>(arr: T[]): T[] {
    return [...new Set(arr)]
}

const unicos = eliminarDuplicados([1, 2, 2, 3, 3, 3])
console.log(unicos)  // [1, 2, 3]

const unicosTexto = eliminarDuplicados(["a", "b", "a"])
console.log(unicosTexto)  // ["a", "b"]

Convenciones de nomenclatura

Las convenciones de nombres para parámetros de tipo ayudan a que el código genérico sea más legible:

// T: tipo general
function procesar<T>(valor: T): T { return valor }

// K, V: clave y valor (colecciones tipo mapa)
type Mapa<K extends string, V> = Record<K, V>

// E: elemento (colecciones)
function primero<E>(arr: E[]): E | undefined { return arr[0] }

// TInput, TOutput: cuando hay transformación
type Convertidor<TInput, TOutput> = (entrada: TInput) => TOutput

// TDatos, TError: tipos de dominio descriptivos
type Respuesta<TDatos, TError = Error> = {
    datos?: TDatos
    error?: TError
}

Usa nombres de una sola letra (T, U, K, V) para genéricos simples. Usa nombres descriptivos con prefijo T (TInput, TOutput, TDatos) cuando hay más de dos parámetros o cuando el contexto requiere claridad adicional.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en TypeScript

Documentación oficial de TypeScript
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de TypeScript

Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender la sintaxis genérica y la inferencia de tipos. Crear funciones, interfaces y alias de tipo genéricos. Aplicar parámetros de tipo por defecto y trabajar con arrays de forma genérica.