Cuando trabajamos con TypeScript, las interfaces nos permiten representar jerarquías y especializaciones de manera clara y estructurada. Un concepto fundamental que podemos aplicar es la herencia entre interfaces, que nos ayuda a definir relaciones jerárquicas y a extender funcionalidades de forma ordenada.
Imaginemos que tenemos una interfaz base llamada Vehicle, que representa un vehículo genérico. En ella podemos declarar propiedades comunes, como el fabricante, que podría ser una cadena de texto. A partir de esta interfaz base, podemos crear interfaces más especializadas, como VehiculoTerrestre y VehiculoMaritimo, que extienden a Vehicle. Esto significa que heredan todas las propiedades y métodos de Vehicle, pero además pueden añadir características propias.
Por ejemplo, VehiculoTerrestre podría incluir un método conducir que no está en la interfaz base, mientras que VehiculoMaritimo podría tener métodos específicos como sonarSirena o encenderChimenea. La palabra clave extends es la que usamos para indicar esta relación de herencia entre interfaces.
interface Vehicle {
readonly fabricante: string;
arrancarMotor(): void;
repostar(): void;
detenerMotor(): void;
}
interface VehiculoTerrestre extends Vehicle {
conducir(): void;
}
interface VehiculoMaritimo extends Vehicle {
sonarSirena(): void;
encenderChimenea(): void;
detenerChimenea(): void;
}
Una vez definidas estas interfaces, podemos implementar una clase que represente un vehículo concreto, como un OpelCorsa, que implemente la interfaz VehiculoTerrestre. Al hacerlo, la clase debe proporcionar implementaciones para todos los métodos declarados en VehiculoTerrestre y también en Vehicle, ya que esta última es la interfaz base.
class OpelCorsa implements VehiculoTerrestre {
readonly fabricante = "Opel";
arrancarMotor(): void {
console.log("Motor arrancando: brum brum");
}
repostar(): void {
console.log("Echando 20 euros de gasolina");
}
detenerMotor(): void {
console.log("Motor detenido: turururu... silencio");
}
conducir(): void {
console.log("Conduciendo el Opel Corsa");
}
}
Este enfoque nos permite encapsular la información y trabajar con abstracciones. Por ejemplo, podemos crear funciones que acepten parámetros de tipo VehiculoTerrestre sin importar si son un OpelCorsa, un patinete eléctrico o cualquier otro vehículo terrestre. Lo importante es que cumplan con la interfaz y tengan los métodos esperados, como conducir o repostar.
function procesarVehiculoTerrestre(vehiculo: VehiculoTerrestre) {
vehiculo.arrancarMotor();
vehiculo.conducir();
vehiculo.repostar();
vehiculo.detenerMotor();
}
const miCoche = new OpelCorsa();
procesarVehiculoTerrestre(miCoche);
Si en lugar de usar VehiculoTerrestre usáramos la interfaz base Vehicle, solo tendríamos acceso a los métodos comunes, sin poder llamar a conducir, que es específico de los vehículos terrestres.
Además, es interesante comparar las interfaces con los alias de tipo (type alias). En versiones recientes de TypeScript, los alias de tipo han ganado capacidades que antes solo tenían las interfaces, como la posibilidad de extender otros tipos usando el operador de intersección &. Esto nos permite replicar jerarquías similares a las que logramos con interfaces.
Por ejemplo, podríamos definir:
type Vehicle = {
readonly fabricante: string;
arrancarMotor(): void;
repostar(): void;
detenerMotor(): void;
};
type VehiculoTerrestre = Vehicle & {
conducir(): void;
};
Así, VehiculoTerrestre combina las propiedades y métodos de Vehicle con los suyos propios. Esta flexibilidad hace que tanto interfaces como tipos puedan ser usados para modelar jerarquías y especializaciones, aunque las interfaces siguen siendo la opción más natural para definir contratos en programación orientada a objetos.
En definitiva, la herencia y especialización de interfaces en TypeScript nos permiten diseñar sistemas más organizados y reutilizables, facilitando la abstracción y el trabajo con diferentes tipos de objetos que comparten características comunes.