Clases abstractas y métodos abstractos
Una clase abstracta es una clase que no puede instanciarse directamente. Su propósito es servir como base para otras clases que implementen los miembros abstractos que define. En TypeScript se declaran con la palabra clave abstract:

abstract class Figura {
abstract calcularArea(): number
abstract calcularPerimetro(): number
describir(): string {
return `Area: ${this.calcularArea().toFixed(2)}, Perimetro: ${this.calcularPerimetro().toFixed(2)}`
}
}
// const figura = new Figura() // Error: Cannot create an instance of an abstract class
Los métodos abstractos no tienen cuerpo de implementación. Cada subclase concreta debe proporcionar su propia implementación:
class Circulo extends Figura {
constructor(private radio: number) {
super()
}
calcularArea(): number {
return Math.PI * this.radio ** 2
}
calcularPerimetro(): number {
return 2 * Math.PI * this.radio
}
}
class Rectangulo extends Figura {
constructor(
private ancho: number,
private alto: number
) {
super()
}
calcularArea(): number {
return this.ancho * this.alto
}
calcularPerimetro(): number {
return 2 * (this.ancho + this.alto)
}
}
const circulo = new Circulo(5)
console.log(circulo.describir()) // "Area: 78.54, Perimetro: 31.42"
const rectangulo = new Rectangulo(4, 6)
console.log(rectangulo.describir()) // "Area: 24.00, Perimetro: 20.00"
Las clases abstractas pueden contener propiedades abstractas además de métodos abstractos:
abstract class Notificacion {
abstract readonly tipo: string
abstract readonly prioridad: number
abstract enviar(destinatario: string): boolean
registrar(destinatario: string): string {
return `[${this.tipo}] Notificacion con prioridad ${this.prioridad} para ${destinatario}`
}
}
class NotificacionEmail extends Notificacion {
readonly tipo = "email"
readonly prioridad = 2
constructor(private asunto: string, private cuerpo: string) {
super()
}
enviar(destinatario: string): boolean {
console.log(`Enviando email a ${destinatario}: ${this.asunto}`)
return true
}
}
class NotificacionSMS extends Notificacion {
readonly tipo = "sms"
readonly prioridad = 3
constructor(private mensaje: string) {
super()
}
enviar(destinatario: string): boolean {
console.log(`Enviando SMS a ${destinatario}: ${this.mensaje}`)
return true
}
}
Si una subclase no implementa todos los miembros abstractos, TypeScript genera un error de compilación. Esto garantiza que el contrato definido por la clase abstracta se cumple siempre.
Cuándo usar abstract class frente a interface
Las clases abstractas e interfaces resuelven problemas diferentes. Las interfaces definen contratos puros sin implementación. Las clases abstractas permiten compartir lógica entre subclases:
// Interface: contrato puro
interface Serializable {
serializar(): string
deserializar(datos: string): void
}
// Clase abstracta: contrato con lógica compartida
abstract class EntidadBase {
readonly creadoEn: Date = new Date()
abstract readonly id: string
abstract validar(): boolean
esReciente(): boolean {
const ahora = new Date()
const diferencia = ahora.getTime() - this.creadoEn.getTime()
return diferencia < 24 * 60 * 60 * 1000 // menos de 24 horas
}
toString(): string {
return `Entidad ${this.id} creada el ${this.creadoEn.toISOString()}`
}
}
Usa interface cuando solo necesites definir la forma de un objeto sin compartir implementación. Usa abstract class cuando necesites combinar un contrato con lógica reutilizable que las subclases hereden.
Una clase puede implementar interfaces y extender una clase abstracta simultáneamente:
interface Auditable {
obtenerHistorial(): string[]
}
interface Exportable {
exportarJSON(): string
}
abstract class Documento {
abstract readonly titulo: string
abstract obtenerContenido(): string
obtenerResumen(): string {
const contenido = this.obtenerContenido()
return contenido.substring(0, 100)
}
}
class Informe extends Documento implements Auditable, Exportable {
readonly titulo: string
private historial: string[] = []
private secciones: string[]
constructor(titulo: string, secciones: string[]) {
super()
this.titulo = titulo
this.secciones = secciones
this.historial.push(`Creado: ${titulo}`)
}
obtenerContenido(): string {
return this.secciones.join("\n")
}
obtenerHistorial(): string[] {
return [...this.historial]
}
exportarJSON(): string {
return JSON.stringify({ titulo: this.titulo, secciones: this.secciones })
}
}
Override y visibilidad en herencia
Palabra clave override
La palabra clave override indica explícitamente que un método sobreescribe a uno de la clase padre. TypeScript verifica que el método efectivamente exista en la clase base, lo que previene errores por nombres mal escritos:
class Animal {
nombre: string
constructor(nombre: string) {
this.nombre = nombre
}
emitirSonido(): string {
return "..."
}
mover(distancia: number): string {
return `${this.nombre} se movio ${distancia} metros`
}
}
class Perro extends Animal {
constructor(nombre: string, private raza: string) {
super(nombre)
}
override emitirSonido(): string {
return "Guau!"
}
override mover(distancia: number): string {
return `${this.nombre} (${this.raza}) corrio ${distancia} metros`
}
// override nadar(): string { } // Error: This member cannot have an 'override' modifier
// because it is not declared in the base class 'Animal'
}
const perro = new Perro("Rex", "Pastor Aleman")
console.log(perro.emitirSonido()) // "Guau!"
console.log(perro.mover(50)) // "Rex (Pastor Aleman) corrio 50 metros"
Se puede activar la opción noImplicitOverride en tsconfig.json para obligar a usar override en toda sobreescritura. De esta forma, si se añade un método en la subclase que coincide con uno de la clase padre sin override, TypeScript emite un error:
// Con noImplicitOverride: true en tsconfig.json
class Gato extends Animal {
emitirSonido(): string { // Error: This member must have an 'override' modifier
return "Miau!" // because it overrides a member in the base class 'Animal'
}
}
Visibilidad en herencia
Los modificadores de visibilidad se heredan y pueden ampliarse (de protected a public) pero no restringirse (de public a private):
class ComponenteBase {
protected estado: string = "inactivo"
protected cambiarEstado(nuevoEstado: string): void {
this.estado = nuevoEstado
}
public obtenerEstado(): string {
return this.estado
}
}
class Boton extends ComponenteBase {
// Ampliamos la visibilidad de protected a public
public override cambiarEstado(nuevoEstado: string): void {
super.cambiarEstado(nuevoEstado)
console.log(`Boton cambio a: ${nuevoEstado}`)
}
hacer_click(): void {
this.cambiarEstado("activo")
}
}
const boton = new Boton()
boton.hacer_click()
console.log(boton.obtenerEstado()) // "activo"
boton.cambiarEstado("deshabilitado") // Ahora es público
Los miembros private de la clase padre son completamente invisibles para las subclases:
class Servicio {
private apiKey: string
protected baseUrl: string
constructor(apiKey: string, baseUrl: string) {
this.apiKey = apiKey
this.baseUrl = baseUrl
}
private construirHeaders(): Record<string, string> {
return { "Authorization": `Bearer ${this.apiKey}` }
}
protected realizarPeticion(ruta: string): string {
const headers = this.construirHeaders()
return `${this.baseUrl}${ruta} con headers: ${JSON.stringify(headers)}`
}
}
class ServicioUsuarios extends Servicio {
constructor(apiKey: string) {
super(apiKey, "https://api.ejemplo.com")
}
obtenerUsuarios(): string {
// Podemos acceder a baseUrl (protected) y realizarPeticion (protected)
return this.realizarPeticion("/usuarios")
// this.apiKey // Error: Property 'apiKey' is private
// this.construirHeaders() // Error: Property 'construirHeaders' is private
}
}
Clases genéricas
Las clases genéricas permiten parametrizar el tipo de datos que manejan, creando componentes reutilizables que mantienen la seguridad de tipos:
class Pila<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]
}
estaVacia(): boolean {
return this.elementos.length === 0
}
tamanio(): number {
return this.elementos.length
}
}
const pilaNumeros = new Pila<number>()
pilaNumeros.push(10)
pilaNumeros.push(20)
console.log(pilaNumeros.pop()) // 20
const pilaTextos = new Pila<string>()
pilaTextos.push("primero")
pilaTextos.push("segundo")
console.log(pilaTextos.peek()) // "segundo"
Restricciones en clases genéricas
Se pueden aplicar restricciones a los parámetros genéricos con extends para garantizar que el tipo cumple ciertos requisitos:
interface ConId {
id: string | number
}
class Repositorio<T extends ConId> {
private elementos: Map<string | number, T> = new Map()
guardar(elemento: T): void {
this.elementos.set(elemento.id, elemento)
}
buscarPorId(id: string | number): T | undefined {
return this.elementos.get(id)
}
eliminar(id: string | number): boolean {
return this.elementos.delete(id)
}
obtenerTodos(): T[] {
return Array.from(this.elementos.values())
}
contar(): number {
return this.elementos.size
}
}
interface Producto {
id: number
nombre: string
precio: number
}
interface Cliente {
id: string
nombre: string
email: string
}
const repoProductos = new Repositorio<Producto>()
repoProductos.guardar({ id: 1, nombre: "Laptop", precio: 999 })
repoProductos.guardar({ id: 2, nombre: "Teclado", precio: 79 })
console.log(repoProductos.buscarPorId(1)) // { id: 1, nombre: "Laptop", precio: 999 }
const repoClientes = new Repositorio<Cliente>()
repoClientes.guardar({ id: "C001", nombre: "Ana", email: "ana@ejemplo.com" })
console.log(repoClientes.contar()) // 1
Clases genéricas con herencia
Las clases genéricas pueden extenderse preservando o concretando los parámetros de tipo:
abstract class ColeccionBase<T> {
protected items: T[] = []
abstract filtrar(predicado: (item: T) => boolean): T[]
agregar(item: T): void {
this.items.push(item)
}
obtenerTodos(): T[] {
return [...this.items]
}
}
class ColeccionOrdenable<T> extends ColeccionBase<T> {
filtrar(predicado: (item: T) => boolean): T[] {
return this.items.filter(predicado)
}
ordenar(comparador: (a: T, b: T) => number): T[] {
return [...this.items].sort(comparador)
}
}
class ColeccionNumerica extends ColeccionBase<number> {
filtrar(predicado: (item: number) => boolean): number[] {
return this.items.filter(predicado)
}
sumar(): number {
return this.items.reduce((acc, val) => acc + val, 0)
}
promedio(): number {
if (this.items.length === 0) return 0
return this.sumar() / this.items.length
}
}
const nombres = new ColeccionOrdenable<string>()
nombres.agregar("Carlos")
nombres.agregar("Ana")
nombres.agregar("Beatriz")
const ordenados = nombres.ordenar((a, b) => a.localeCompare(b))
console.log(ordenados) // ["Ana", "Beatriz", "Carlos"]
const números = new ColeccionNumerica()
números.agregar(10)
números.agregar(20)
números.agregar(30)
console.log(números.sumar()) // 60
console.log(números.promedio()) // 20
Un ejemplo que combina clases abstractas, genéricos y herencia tipada:
abstract class Procesador<TEntrada, TSalida> {
abstract procesar(entrada: TEntrada): TSalida
procesarLote(entradas: TEntrada[]): TSalida[] {
return entradas.map(entrada => this.procesar(entrada))
}
}
class ConversorMayusculas extends Procesador<string, string> {
procesar(entrada: string): string {
return entrada.toUpperCase()
}
}
class ParseadorNumeros extends Procesador<string, number> {
procesar(entrada: string): number {
const resultado = parseFloat(entrada)
if (isNaN(resultado)) {
throw new Error(`No se puede convertir "${entrada}" a número`)
}
return resultado
}
}
interface DatosUsuario {
nombre: string
edad: string
activo: string
}
interface UsuarioValidado {
nombre: string
edad: number
activo: boolean
}
class ValidadorUsuarios extends Procesador<DatosUsuario, UsuarioValidado> {
procesar(entrada: DatosUsuario): UsuarioValidado {
return {
nombre: entrada.nombre.trim(),
edad: parseInt(entrada.edad, 10),
activo: entrada.activo === "true"
}
}
}
const conversor = new ConversorMayusculas()
console.log(conversor.procesarLote(["hola", "mundo"])) // ["HOLA", "MUNDO"]
const parseador = new ParseadorNumeros()
console.log(parseador.procesarLote(["3.14", "42", "0.5"])) // [3.14, 42, 0.5]
const validador = new ValidadorUsuarios()
const usuarios = validador.procesarLote([
{ nombre: "Ana", edad: "28", activo: "true" },
{ nombre: "Luis", edad: "35", activo: "false" }
])
console.log(usuarios)
// [{ nombre: "Ana", edad: 28, activo: true }, { nombre: "Luis", edad: 35, activo: false }]
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 las clases abstractas y los métodos abstractos en TypeScript. Diferenciar entre abstract class e interface y saber cuando usar cada una. Tipar jerarquías de herencia con visibilidad controlada. Aplicar la palabra clave override para sobreescribir métodos. Implementar clases genericas con restricciones de tipo.