Las clases abstractas en TypeScript te permiten definir una estructura compartida para un grupo de clases relacionadas sin comprometerte con una implementación completa desde el inicio. Son uno de esos recursos que parecen académicos hasta que te encuentras manteniendo una base de código con seis clases ligeramente distintas pero que deberían ser consistentes, y te das cuenta de que las necesitabas hace tres meses.
Entendiendo las Clases Abstractas
Una clase abstracta no puede instanciarse directamente. Es un blueprint del que otras clases heredan. Puede contener métodos completamente implementados junto con métodos abstractos, que son declaraciones sin cuerpo que las subclases deben implementar.
TypeScript usa la palabra clave abstract para marcar tanto la clase como los métodos que requieren implementación en las subclases.
Clases Abstractas en TypeScript
Aquí hay un ejemplo sencillo:
abstract class Animal {
abstract makeSound(): void; // Abstract method
move(): void {
console.log("Moving along!");
}
}
Animal es abstracta: declara makeSound() sin implementarlo y provee un método concreto move(). Cualquier clase que extienda Animal debe suministrar su propia implementación de makeSound().
Así es como podrías extender la clase Animal:
class Dog extends Animal {
makeSound(): void {
console.log("Bark!");
}
}
const myDog = new Dog();
myDog.makeSound(); // Outputs: Bark!
myDog.move(); // Outputs: Moving along!
Dog obtiene move() de forma gratuita y satisface el contrato de makeSound(). Intenta instanciar Animal directamente y el compilador de TypeScript lo rechaza en tiempo de build, antes de que cualquier bug llegue a producción.
Cuándo Usar Clases Abstractas
Las clases abstractas funcionan mejor cuando un grupo de clases comparte implementación real, no solo un contrato. Un ejemplo común son los vehículos con secuencias de arranque distintas:
abstract class Vehicle {
abstract startEngine(): void;
stopEngine(): void {
console.log("Engine stopped.");
}
}
Car y Motorcycle implementarían startEngine() de forma diferente, pero ambos heredan el mismo stopEngine() sin duplicación.
Si las clases no comparten ninguna implementación, opta por una interfaz.
Ventajas de Usar Clases Abstractas
- La lógica compartida reside en un único lugar. Cambia
stopEngine()una vez y todas las subclases reciben la actualización. - Polimorfismo: el código que referencia el tipo abstracto funciona con cualquier subclase concreta.
- El compilador impone el contrato. Las implementaciones faltantes son errores de compilación, no sorpresas en tiempo de ejecución.
Clases Abstractas vs. Interfaces
Ambas definen contratos, pero la distinción importa en la práctica.
Las clases abstractas pueden contener métodos implementados y estado de instancia. Las interfaces son puramente estructurales: declaran cómo debe lucir un tipo, sin ninguna implementación.
Elige una clase abstracta cuando tengas código compartido que distribuir. Elige una interfaz cuando estés describiendo una capacidad que clases no relacionadas podrían adoptar de forma independiente.
Ejemplo Real: Implementando una Jerarquía de Figuras
Considera una aplicación que trabaja con figuras geométricas. Puedes definir una clase abstracta Shape:
abstract class Shape {
abstract area(): number;
toString(): string {
return `Shape with area ${this.area()}`;
}
}
Subclases como Circle y Rectangle implementarían el método area():
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
area(): number {
return this.width * this.height;
}
}
const circle = new Circle(5);
console.log(circle.toString()); // Outputs: Shape with area 78.53981633974483
const rectangle = new Rectangle(10, 5);
console.log(rectangle.toString()); // Outputs: Shape with area 50
toString() se escribe una sola vez en Shape y llama al area() que provee la clase concreta. Agregar un Triangle más adelante implica implementar un único método, no copiar boilerplate.
Conclusión
Las clases abstractas son la herramienta adecuada cuando tienes implementación compartida que distribuir y un contrato que imponer. Detectan implementaciones de métodos faltantes en tiempo de compilación y mantienen la lógica común en un único lugar. Si solo necesitas describir una forma, usa una interfaz. Si necesitas compartir código e imponer una forma, usa una clase abstracta.