
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 ConLongitudgarantiza que dentro de la función se puede acceder aarg.lengthde 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
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.