Genéricos con clases e interfaces

Intermedio
TypeScript
TypeScript
Actualizado: 04/05/2026

Diagrama: tutorial-typescript-genericos-clases-interfaces

Clases genéricas

Las clases genéricas permiten definir estructuras de datos y comportamientos que operan sobre tipos parametrizados. El parámetro de tipo se declara junto al nombre de la clase entre corchetes angulares <T> y puede usarse en propiedades, métodos, constructores y tipos de retorno.

Clase Stack genérica

Una pila (stack) es una estructura de datos LIFO (Last In, First Out) que se beneficia enormemente de los genéricos:

class Stack<T> {
    private elementos: T[] = []

    push(elemento: T): void {
        this.elementos.push(elemento)
    }

    pop(): T | undefined {
        return this.elementos.pop()
    }

    peek(): T | undefined {
        return this.elementos[this.elementos.length - 1]
    }

    get tamano(): number {
        return this.elementos.length
    }

    estaVacia(): boolean {
        return this.elementos.length === 0
    }
}

const pilaNumeros = new Stack<number>()
pilaNumeros.push(10)
pilaNumeros.push(20)
pilaNumeros.push(30)

console.log(pilaNumeros.peek())   // 30
console.log(pilaNumeros.pop())    // 30
console.log(pilaNumeros.tamano)   // 2

const pilaTextos = new Stack<string>()
pilaTextos.push("primero")
pilaTextos.push("segundo")
console.log(pilaTextos.pop())     // "segundo"

Las clases genéricas son genéricas solo en su parte de instancia. Los miembros estáticos no pueden usar el parámetro de tipo de la clase.

Clase Queue genérica

Una cola (queue) implementa el patrón FIFO (First In, First Out):

class Queue<T> {
    private elementos: T[] = []

    enqueue(elemento: T): void {
        this.elementos.push(elemento)
    }

    dequeue(): T | undefined {
        return this.elementos.shift()
    }

    frente(): T | undefined {
        return this.elementos[0]
    }

    get tamano(): number {
        return this.elementos.length
    }
}

const colaTareas = new Queue<string>()
colaTareas.enqueue("compilar")
colaTareas.enqueue("testear")
colaTareas.enqueue("desplegar")

console.log(colaTareas.dequeue())  // "compilar"
console.log(colaTareas.frente())   // "testear"
console.log(colaTareas.tamano)     // 2

Clase Repository genérica

Un repositorio encapsula operaciones CRUD sobre entidades tipadas:

interface Entidad {
    id: number
}

class Repository<T extends Entidad> {
    private items: Map<number, T> = new Map()

    guardar(item: T): void {
        this.items.set(item.id, item)
    }

    buscarPorId(id: number): T | undefined {
        return this.items.get(id)
    }

    listar(): T[] {
        return Array.from(this.items.values())
    }

    eliminar(id: number): boolean {
        return this.items.delete(id)
    }

    existe(id: number): boolean {
        return this.items.has(id)
    }
}

interface Usuario extends Entidad {
    nombre: string
    email: string
}

interface Producto extends Entidad {
    nombre: string
    precio: number
}

const repoUsuarios = new Repository<Usuario>()
repoUsuarios.guardar({ id: 1, nombre: "Ana", email: "ana@ejemplo.com" })
repoUsuarios.guardar({ id: 2, nombre: "Luis", email: "luis@ejemplo.com" })

console.log(repoUsuarios.buscarPorId(1))  // { id: 1, nombre: "Ana", ... }
console.log(repoUsuarios.listar().length)  // 2

const repoProductos = new Repository<Producto>()
repoProductos.guardar({ id: 1, nombre: "Teclado", precio: 50 })
console.log(repoProductos.buscarPorId(1)?.precio)  // 50

Interfaces genéricas

Las interfaces genéricas definen contratos que se adaptan al tipo proporcionado. A diferencia de las clases, las interfaces no generan código en tiempo de ejecución, pero garantizan que las implementaciones cumplan el contrato tipado.

Interfaces para estructuras de datos

interface Comparable<T> {
    comparar(otro: T): number
}

class Temperatura implements Comparable<Temperatura> {
    constructor(public grados: number) {}

    comparar(otra: Temperatura): number {
        return this.grados - otra.grados
    }
}

const t1 = new Temperatura(20)
const t2 = new Temperatura(35)
console.log(t1.comparar(t2))  // -15 (t1 es menor)
interface Serializable<T> {
    serializar(): string
    deserializar(datos: string): T
}

class Configuracion implements Serializable<Configuracion> {
    constructor(
        public tema: string,
        public idioma: string
    ) {}

    serializar(): string {
        return JSON.stringify({ tema: this.tema, idioma: this.idioma })
    }

    deserializar(datos: string): Configuracion {
        const obj = JSON.parse(datos)
        return new Configuracion(obj.tema, obj.idioma)
    }
}

const config = new Configuracion("oscuro", "es")
const json = config.serializar()
console.log(json)  // '{"tema":"oscuro","idioma":"es"}'

Interfaces con múltiples parámetros de tipo

interface Mapeador<TEntrada, TSalida> {
    mapear(entrada: TEntrada): TSalida
    mapearLista(entradas: TEntrada[]): TSalida[]
}

interface UsuarioDTO {
    id: number
    nombreCompleto: string
    correo: string
}

interface UsuarioVista {
    nombre: string
    iniciales: string
}

class UsuarioMapeador implements Mapeador<UsuarioDTO, UsuarioVista> {
    mapear(entrada: UsuarioDTO): UsuarioVista {
        const partes = entrada.nombreCompleto.split(" ")
        return {
            nombre: entrada.nombreCompleto,
            iniciales: partes.map(p => p[0]).join("")
        }
    }

    mapearLista(entradas: UsuarioDTO[]): UsuarioVista[] {
        return entradas.map(e => this.mapear(e))
    }
}

const mapeador = new UsuarioMapeador()
const vista = mapeador.mapear({
    id: 1,
    nombreCompleto: "Ana Garcia",
    correo: "ana@ejemplo.com"
})
console.log(vista)  // { nombre: "Ana Garcia", iniciales: "AG" }

Interfaces genéricas para respuestas tipadas

interface RespuestaPaginada<T> {
    datos: T[]
    pagina: number
    totalPaginas: number
    totalElementos: number
}

function crearRespuesta<T>(
    datos: T[],
    pagina: number,
    porPagina: number,
    total: number
): RespuestaPaginada<T> {
    return {
        datos,
        pagina,
        totalPaginas: Math.ceil(total / porPagina),
        totalElementos: total
    }
}

const respuesta = crearRespuesta(
    [{ id: 1, titulo: "Post 1" }, { id: 2, titulo: "Post 2" }],
    1,
    10,
    25
)
console.log(respuesta.totalPaginas)  // 3

Restricciones genéricas con extends

La palabra clave extends en el contexto de genéricos establece una restricción sobre los tipos que pueden usarse como argumento de tipo. El tipo proporcionado debe ser asignable al tipo de la restricción.

Restricciones con interfaces

interface ConLongitud {
    length: number
}

function registrarLongitud<T extends ConLongitud>(arg: T): T {
    console.log(`Longitud: ${arg.length}`)
    return arg
}

registrarLongitud("hola")           // Longitud: 4
registrarLongitud([1, 2, 3])        // Longitud: 3
registrarLongitud({ length: 10 })   // Longitud: 10

// Error: number no tiene propiedad length
// registrarLongitud(42)

La restricción T extends ConLongitud garantiza que dentro de la función se puede acceder a arg.length de forma segura. TypeScript rechaza en tiempo de compilación cualquier tipo que no cumpla esta restricción.

Restricciones con tipos primitivos

function formatear<T extends string | number>(valor: T): string {
    if (typeof valor === "string") {
        return valor.toUpperCase()
    }
    return valor.toFixed(2)
}

console.log(formatear("hola"))   // "HOLA"
console.log(formatear(3.14159))  // "3.14"

// Error: boolean no extiende string | number
// formatear(true)

Restricciones en clases genéricas

interface Identificable {
    id: number
}

class AlmacenOrdenado<T extends Identificable> {
    private items: T[] = []

    agregar(item: T): void {
        this.items.push(item)
        this.items.sort((a, b) => a.id - b.id)
    }

    buscar(id: number): T | undefined {
        return this.items.find(item => item.id === id)
    }

    listar(): T[] {
        return [...this.items]
    }
}

interface Tarea extends Identificable {
    titulo: string
    completada: boolean
}

const almacen = new AlmacenOrdenado<Tarea>()
almacen.agregar({ id: 3, titulo: "Revisar", completada: false })
almacen.agregar({ id: 1, titulo: "Planificar", completada: true })
almacen.agregar({ id: 2, titulo: "Desarrollar", completada: false })

console.log(almacen.listar().map(t => t.titulo))
// ["Planificar", "Desarrollar", "Revisar"]

Restricción keyof

El operador keyof combinado con restricciones genéricas permite crear funciones que acceden a propiedades de objetos de forma completamente tipada:

function obtenerPropiedad<T, K extends keyof T>(obj: T, clave: K): T[K] {
    return obj[clave]
}

const usuario = {
    id: 1,
    nombre: "Carlos",
    email: "carlos@ejemplo.com",
    activo: true
}

const nombre = obtenerPropiedad(usuario, "nombre")   // string
const activo = obtenerPropiedad(usuario, "activo")    // boolean

// Error: "edad" no existe en keyof typeof usuario
// obtenerPropiedad(usuario, "edad")

La restricción K extends keyof T garantiza que solo se pueden pasar claves que realmente existen en el objeto. El tipo de retorno T[K] es un tipo de acceso indexado que devuelve el tipo exacto de esa propiedad.

function establecerPropiedad<T, K extends keyof T>(
    obj: T,
    clave: K,
    valor: T[K]
): void {
    obj[clave] = valor
}

const config = { host: "localhost", puerto: 3000, debug: false }

establecerPropiedad(config, "host", "192.168.1.1")    // OK
establecerPropiedad(config, "puerto", 8080)            // OK
establecerPropiedad(config, "debug", true)             // OK

// Error: no se puede asignar string a number
// establecerPropiedad(config, "puerto", "8080")

Seleccionar múltiples propiedades

function seleccionar<T, K extends keyof T>(
    obj: T,
    claves: K[]
): Pick<T, K> {
    const resultado = {} as Pick<T, K>
    for (const clave of claves) {
        resultado[clave] = obj[clave]
    }
    return resultado
}

const persona = {
    id: 1,
    nombre: "Elena",
    email: "elena@ejemplo.com",
    edad: 30,
    ciudad: "Madrid"
}

const contacto = seleccionar(persona, ["nombre", "email"])
// tipo: Pick<typeof persona, "nombre" | "email">
console.log(contacto)  // { nombre: "Elena", email: "elena@ejemplo.com" }

Múltiples parámetros de tipo en clases

Las clases pueden tener varios parámetros de tipo para modelar relaciones complejas entre datos:

class Diccionario<K extends string | number, V> {
    private datos = new Map<K, V>()

    establecer(clave: K, valor: V): void {
        this.datos.set(clave, valor)
    }

    obtener(clave: K): V | undefined {
        return this.datos.get(clave)
    }

    tiene(clave: K): boolean {
        return this.datos.has(clave)
    }

    entradas(): [K, V][] {
        return Array.from(this.datos.entries())
    }
}

const traducciones = new Diccionario<string, string>()
traducciones.establecer("hello", "hola")
traducciones.establecer("world", "mundo")
console.log(traducciones.obtener("hello"))  // "hola"

const inventario = new Diccionario<number, { nombre: string; stock: number }>()
inventario.establecer(1001, { nombre: "Laptop", stock: 15 })
console.log(inventario.obtener(1001)?.stock)  // 15

Clase con transformación entre tipos

class Adaptador<TFuente, TDestino> {
    constructor(private transformar: (fuente: TFuente) => TDestino) {}

    convertir(fuente: TFuente): TDestino {
        return this.transformar(fuente)
    }

    convertirLista(fuentes: TFuente[]): TDestino[] {
        return fuentes.map(f => this.transformar(f))
    }
}

interface DatosAPI {
    user_name: string
    user_email: string
    is_active: boolean
}

interface UsuarioApp {
    nombre: string
    email: string
    activo: boolean
}

const adaptador = new Adaptador<DatosAPI, UsuarioApp>(datos => ({
    nombre: datos.user_name,
    email: datos.user_email,
    activo: datos.is_active
}))

const usuario = adaptador.convertir({
    user_name: "Ana",
    user_email: "ana@ejemplo.com",
    is_active: true
})
console.log(usuario)  // { nombre: "Ana", email: "ana@ejemplo.com", activo: true }

Genéricos con utility types

Los utility types de TypeScript se combinan con genéricos propios para crear tipos derivados útiles:

class FormularioBase<T> {
    private valores: Partial<T> = {}
    private errores: Partial<Record<keyof T, string>> = {}

    establecer<K extends keyof T>(campo: K, valor: T[K]): void {
        this.valores[campo] = valor
    }

    obtener<K extends keyof T>(campo: K): T[K] | undefined {
        return this.valores[campo]
    }

    setError<K extends keyof T>(campo: K, mensaje: string): void {
        (this.errores as Record<keyof T, string>)[campo] = mensaje
    }

    getError<K extends keyof T>(campo: K): string | undefined {
        return this.errores[campo]
    }

    esValido(): boolean {
        return Object.keys(this.errores).length === 0
    }

    obtenerDatos(): Partial<T> {
        return { ...this.valores }
    }
}

interface DatosRegistro {
    nombre: string
    email: string
    edad: number
}

const formulario = new FormularioBase<DatosRegistro>()
formulario.establecer("nombre", "Luis")
formulario.establecer("email", "luis@ejemplo.com")
formulario.establecer("edad", 25)

console.log(formulario.obtener("nombre"))  // "Luis"
console.log(formulario.obtenerDatos())
// { nombre: "Luis", email: "luis@ejemplo.com", edad: 25 }

Crear tipos derivados con genéricos

type SoloLectura<T> = {
    readonly [K in keyof T]: T[K]
}

type Validador<T> = {
    [K in keyof T]: (valor: T[K]) => boolean
}

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

const validadores: Validador<Producto> = {
    nombre: (v) => v.length > 0,
    precio: (v) => v > 0,
    stock: (v) => v >= 0
}

function validar<T>(datos: T, validadores: Validador<T>): boolean {
    for (const clave in validadores) {
        const validador = validadores[clave]
        if (!validador(datos[clave])) {
            return false
        }
    }
    return true
}

const producto: Producto = { nombre: "Monitor", precio: 300, stock: 10 }
console.log(validar(producto, validadores))  // true

const invalido: Producto = { nombre: "", precio: -5, stock: 10 }
console.log(validar(invalido, validadores))  // false

Patrón Builder genérico

class Builder<T extends Record<string, unknown>> {
    private datos: Partial<T> = {}

    set<K extends keyof T>(clave: K, valor: T[K]): this {
        this.datos[clave] = valor
        return this
    }

    build(): T {
        return this.datos as T
    }
}

interface Solicitud {
    url: string
    metodo: string
    cabeceras: Record<string, string>
    cuerpo: string
}

const solicitud = new Builder<Solicitud>()
    .set("url", "https://api.ejemplo.com/datos")
    .set("metodo", "POST")
    .set("cabeceras", { "Content-Type": "application/json" })
    .set("cuerpo", '{"clave": "valor"}')
    .build()

console.log(solicitud.url)     // "https://api.ejemplo.com/datos"
console.log(solicitud.metodo)  // "POST"

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

Crear clases genéricas como Stack y Repository. Definir interfaces genéricas con contratos flexibles. Aplicar restricciones con extends y keyof. Combinar múltiples parámetros de tipo en clases e interfaces.