Clases abstractas: contratos para subclases
Las clases abstractas definen una estructura base que no puede instanciarse directamente. Obligan a las subclases a implementar ciertos métodos, garantizando un contrato común.
¿Qué es una clase abstracta?
Una clase abstracta es un molde incompleto — define la estructura y parte del comportamiento, pero deja ciertos métodos sin implementar para que las subclases los completen. No se puede instanciar directamente:
abstract class Figura {
// Esta clase existe solo para ser extendida
}
// ❌ Error: Cannot create an instance of an abstract class
// const f = new Figura();
class Circulo extends Figura {}
// ✅ Instanciar la subclase concreta sí está permitido
const c = new Circulo();
console.log(c instanceof Figura); // true — sigue siendo una FiguratrueLas clases abstractas modelan conceptos que no tienen sentido solos. Un "Animal" genérico no tiene sonido propio, pero Perro y Gato sí. La clase abstracta garantiza que toda subclase implemente lo que le falta.
abstract class en TypeScript
Una clase abstracta puede tener propiedades, métodos concretos y métodos abstractos mezclados:
abstract class Vehiculo {
// Propiedades concretas — disponibles en todas las subclases
readonly marca: string;
readonly modelo: string;
protected velocidadActual: number = 0;
constructor(marca: string, modelo: string) {
this.marca = marca;
this.modelo = modelo;
}
// Método concreto — lógica compartida
acelerar(incremento: number): void {
this.velocidadActual += incremento;
console.log(`${this.marca} ${this.modelo}: ${this.velocidadActual} km/h`);
}
// Método abstracto — cada subclase debe implementarlo
abstract combustible(): string;
abstract autonomiaKm(): number;
}
class AutoElectrico extends Vehiculo {
private bateriaKwh: number;
constructor(marca: string, modelo: string, bateriaKwh: number) {
super(marca, modelo);
this.bateriaKwh = bateriaKwh;
}
combustible(): string {
return "Electricidad";
}
autonomiaKm(): number {
return this.bateriaKwh * 6; // ~6 km por kWh
}
}
const tesla = new AutoElectrico("Tesla", "Model 3", 75);
tesla.acelerar(60);
console.log(tesla.combustible()); // "Electricidad"
console.log(tesla.autonomiaKm()); // 450Tesla Model 3: 60 km/h
Electricidad
450Métodos abstractos: firma sin implementación
Los métodos abstractos declaran la firma (nombre, parámetros, tipo de retorno) pero no tienen cuerpo. La subclase está obligada a implementarlos:
abstract class Reporte {
private titulo: string;
private fecha: Date;
constructor(titulo: string) {
this.titulo = titulo;
this.fecha = new Date();
}
// Métodos abstractos: cada tipo de reporte define su contenido
abstract generarCabecera(): string;
abstract generarCuerpo(): string;
abstract generarPie(): string;
// Método concreto que usa los abstractos (Template Method pattern)
generar(): string {
return [
this.generarCabecera(),
this.generarCuerpo(),
this.generarPie(),
`Generado: ${this.fecha.toLocaleDateString()}`,
].join("\n");
}
}
class ReportePDF extends Reporte {
private datos: string[];
constructor(titulo: string, datos: string[]) {
super(titulo);
this.datos = datos;
}
generarCabecera(): string {
return "=== REPORTE PDF ===";
}
generarCuerpo(): string {
return this.datos.map((d, i) => `${i + 1}. ${d}`).join("\n");
}
generarPie(): string {
return `Total de registros: ${this.datos.length}`;
}
}
const reporte = new ReportePDF("Ventas Q1", ["Laptop: $1200", "Mouse: $25", "Teclado: $80"]);
console.log(reporte.generar());=== REPORTE PDF ===
1. Laptop: $1200
2. Mouse: $25
3. Teclado: $80
Total de registros: 3
Generado: 02/05/2026Si tu subclase concreta no implementa todos los métodos abstractos del padre, TypeScript lanzará un error en compilación: Non-abstract class 'X' does not implement inherited abstract member 'y'. Esto es una garantía valiosa.
Métodos concretos en abstractas
Las clases abstractas pueden tener métodos completamente implementados que las subclases heredan y pueden usar (o hacer override):
abstract class Autenticador {
// Método concreto: flujo fijo de autenticación
async autenticar(usuario: string, contrasena: string): Promise<boolean> {
// 1. Paso concreto: log de intento
this.registrarIntento(usuario);
// 2. Paso abstracto: cada subclase valida a su manera
const esValido = await this.validarCredenciales(usuario, contrasena);
// 3. Paso concreto: manejar resultado
if (esValido) {
this.registrarExito(usuario);
} else {
this.registrarFallo(usuario);
}
return esValido;
}
// Métodos abstractos: la subclase define el "cómo"
protected abstract validarCredenciales(usuario: string, contrasena: string): Promise<boolean>;
// Métodos concretos con comportamiento por defecto
protected registrarIntento(usuario: string): void {
console.log(`[AUTH] Intento de login: ${usuario}`);
}
protected registrarExito(usuario: string): void {
console.log(`[AUTH] Login exitoso: ${usuario}`);
}
protected registrarFallo(usuario: string): void {
console.log(`[AUTH] Login fallido: ${usuario}`);
}
}
class AutenticadorLocal extends Autenticador {
private usuarios = new Map([["admin", "1234"], ["ana", "pass"]]);
protected async validarCredenciales(usuario: string, contrasena: string): Promise<boolean> {
return this.usuarios.get(usuario) === contrasena;
}
}
const auth = new AutenticadorLocal();
await auth.autenticar("admin", "1234"); // exitoso
await auth.autenticar("hacker", "0000"); // fallido[AUTH] Intento de login: admin
[AUTH] Login exitoso: admin
[AUTH] Intento de login: hacker
[AUTH] Login fallido: hackerAbstractas vs interfaces: cuándo usar cada una
Ambas definen contratos, pero tienen capacidades distintas. La elección correcta depende del contexto:
| Característica | Clase abstracta | Interface |
|---|---|---|
| Implementación de métodos | Sí — puede tener código | No — solo firmas |
| Estado (propiedades con valor) | Sí | No (solo tipos) |
| Constructor | Sí | No |
| Herencia múltiple | No — solo una clase padre | Sí — múltiples interfaces |
| Modificadores de acceso | Sí (private, protected) | No — todo es público |
| Cuando usar | Jerarquía con código compartido | Contrato puro, sin lógica compartida |
// ✅ Usa interface cuando defines solo la forma del objeto
interface Serializable {
toJSON(): object;
fromJSON(data: object): void;
}
// ✅ Usa abstract class cuando compartes lógica entre subclases
abstract class BaseDatos {
private conectado = false;
async conectar(): Promise<void> {
await this.establecerConexion(); // abstracto
this.conectado = true;
console.log("Conectado");
}
protected abstract establecerConexion(): Promise<void>;
abstract consultar(sql: string): Promise<unknown[]>;
}
// ✅ Puedes combinar ambas: abstracta que implementa interfaces
abstract class Repositorio implements Serializable {
abstract toJSON(): object;
abstract fromJSON(data: object): void;
abstract guardar(): Promise<void>;
}
console.log("TypeScript te da todas las herramientas");TypeScript te da todas las herramientas- Usa
abstract classcuando necesitas compartir código entre clases relacionadas - Usa
interfacecuando solo necesitas definir la forma de un objeto o un contrato sin lógica - Una clase puede implementar múltiples interfaces pero solo extender una clase abstracta
- Los métodos abstractos obligan a las subclases a completar el contrato — úsalos en lugar de lanzar errores manualmente