Clases: herencia

La herencia es una propiedad importante que tiene la orientación a objetos que permite a las clases especializarse y fabricar jerarquías, sustituyendo sus comportamientos generales por otros específicos a medida que se baja en la jerarquía. En este capítulo hago un sistema de clases que usa herencia en TypeScript.

La herencia en TypeScript nos permite construir jerarquías de clases que reflejan relaciones de especialización entre ellas. Por ejemplo, podemos tener una clase base llamada Vehículo y derivar de ella otras clases como VehículoTerrestre, VehículoAéreo y VehículoMarítimo. Estas subclases heredan los atributos y métodos de la clase base, pero también pueden modificar o ampliar su comportamiento para adaptarse a sus particularidades.

Para establecer esta relación de herencia usamos la palabra clave extends. Así, al declarar class VehículoTerrestre extends Vehículo, indicamos que VehículoTerrestre es una especialización de Vehículo. Esto significa que VehículoTerrestre hereda todo lo que tiene Vehículo, pero puede redefinir métodos para cambiar su comportamiento.

Imaginemos que en la clase Vehículo tenemos un método llamado moverse que simplemente imprime un mensaje genérico:

class Vehículo {
  moverse() {
    console.log("El vehículo se ha movido mágicamente");
  }
}

Si instanciamos un objeto de esta clase y llamamos a moverse, veremos ese mensaje. Ahora, si creamos una subclase VehículoTerrestre que hereda de Vehículo, esta también tendrá el método moverse por herencia y, por defecto, se comportará igual:

class VehículoTerrestre extends Vehículo {}

Pero podemos redefinir el método moverse en VehículoTerrestre para que imprima algo más específico, como un sonido característico:

class VehículoTerrestre extends Vehículo {
  moverse() {
    console.log("Brum brum");
  }
}

Así, cuando llamemos a moverse en una instancia de VehículoTerrestre, veremos el mensaje personalizado. Esto nos permite adaptar el comportamiento de la clase base en las subclases.

Además, para mantener la conexión con la clase base y aprovechar su funcionalidad, podemos usar la palabra clave super. Esta referencia nos permite invocar métodos o constructores de la clase superior. Por ejemplo, dentro de VehículoTerrestre podemos llamar a super.moverse() para ejecutar el método original de Vehículo:

class VehículoTerrestre extends Vehículo {
  moverse() {
    super.moverse();
    console.log("Brum brum");
  }
}

De esta forma, combinamos el comportamiento de la clase base con el específico de la subclase.

Podemos aplicar esta misma lógica a otras subclases, como VehículoAéreo o VehículoMarítimo, cada una con su propia implementación de moverse que refleje sus características particulares:

class VehículoAéreo extends Vehículo {
  moverse() {
    console.log("Zuuum");
  }
}

class VehículoMarítimo extends Vehículo {
  moverse() {
    console.log("Ruido del mar");
  }
}

Otro aspecto importante en la herencia es el manejo de constructores y atributos. Supongamos que queremos que cada vehículo tenga un atributo fabricante de tipo string. Lo declaramos en la clase base y le añadimos un constructor para inicializarlo:

class Vehículo {
  fabricante: string;

  constructor(fabricante: string) {
    this.fabricante = fabricante;
  }

  moverse() {
    console.log(`El vehículo ${this.fabricante} se ha movido mágicamente`);
  }
}

Cuando creamos una subclase que tiene su propio constructor, debemos llamar al constructor de la clase base usando super, pasando los parámetros necesarios. Por ejemplo, en VehículoTerrestre podemos añadir un atributo adicional tipo para especificar si es un tanque, una moto o un coche:

class VehículoTerrestre extends Vehículo {
  tipo: string;

  constructor(fabricante: string, tipo: string) {
    super(fabricante);
    this.tipo = tipo;
  }

  moverse() {
    console.log(`Brum, brum, hace mi ${this.tipo}`);
    super.moverse();
  }
}

Al instanciar un VehículoTerrestre, debemos proporcionar tanto el fabricante como el tipo:

const miMoto = new VehículoTerrestre("Yamaha", "moto");
miMoto.moverse();
// Salida:
// Brum, brum, hace mi moto
// El vehículo Yamaha se ha movido mágicamente

Este patrón garantiza que la información común se maneje en la clase base, mientras que las subclases pueden extenderla con detalles específicos. Además, TypeScript nos obliga a llamar al constructor de la clase superior cuando definimos un constructor en una subclase, asegurando que la inicialización se realice correctamente.

En resumen, la herencia en TypeScript nos permite crear estructuras jerárquicas de clases que comparten atributos y métodos, pero que también pueden especializar su comportamiento. Usando extends para definir la relación, super para acceder a la clase base y constructores que respetan esta jerarquía, podemos diseñar sistemas flexibles y reutilizables que reflejen las relaciones naturales entre objetos.

Lista de reproducción
  1. 1
    Temporada 1
    5 minutos
  2. 2
    ¿Qué es TypeScript?
    11 minutos
  3. 3
    Instalando TypeScript
    8 minutos
  4. 4
    Compilando un Hola Mundo sencillo
    7 minutos
  5. 5
    Hola Mundo pero con tipos
    10 minutos
  6. 6
    Tipos: tipos primitivos
    12 minutos
  7. 7
    Tipos: tipos especiales (any, null, ...)
    10 minutos
  8. 8
    Tipos: arrays y tuplas
    11 minutos
  9. 9
    Tipos: objetos
    7 minutos
  10. 10
    Funciones: lo básico
    9 minutos
  11. 11
    Funciones: tipando funciones
    9 minutos
  12. 12
    Clases: introducción a las clases
    9 minutos
  13. 13
    Clases: creando una clase
    10 minutos
  14. 14
    Clases: modificador private
    8 minutos
  15. 15
    Clases: modificador readonly
    3 minutos
  16. 16
    Clases: Atributos virtuales con getters y setters
    10 minutos
  17. 17
    Clases: herencia
    9 minutos
  18. 18
    Clases: modificadores abstract y protected
    8 minutos
  19. 19
    Tipos alias
    6 minutos
  20. 20
    Tipos literales
    5 minutos
  21. 21
    Uniones de tipos
    7 minutos
  22. 22
    Uniones discriminantes
    7 minutos
  23. 23
    Intersecciones de tipos
    5 minutos
  24. 24
    Interfaces: introducción
    7 minutos
  25. 25
    Interfaces: modificadores y funciones
    9 minutos
  26. 26
    Interfaces: usándolas con clases
    8 minutos
  27. 27
    Interfaces: herencia de interfaces
    8 minutos
  28. 28
    Interfaces: interfaces indizadas
    5 minutos
  29. 29
    Interfaces: funciones y tipos híbridos
    5 minutos
  30. 30
    ¿Qué diferencia hay entre interfaces y tipos? (2020)
    8 minutos
  31. 31
    Casteos con as
    6 minutos
  32. 32
    instanceof y las guardas
    9 minutos
  33. 33
    Tipos enumerados
    8 minutos
  34. 34
    Valores avanzados para enumerados
    7 minutos
  35. 35
    Enumerados con valores computados
    6 minutos
  36. 36
    Genéricos en tipos
    8 minutos
  37. 37
    Múltiples genéricos y buenas prácticas
    5 minutos
  38. 38
    Genéricos en funciones
    8 minutos
  39. 39
    Genéricos con restricciones
    6 minutos
  40. 40
    Tipos de utilidad
    3 minutos
  41. 41
    Exportando módulos
    9 minutos
  42. 42
    Importando módulos
    7 minutos
  43. 43
    Export default e import asterisco
    6 minutos
  44. 44
    tsconfig
    7 minutos
  45. 45
    Módulos desde NPM
    7 minutos
  46. 46
    Arroba types y los .d.ts
    8 minutos
  47. 47
    Ejemplo (1): creando una API REST simple en TypeScript
    11 minutos
  48. 48
    Ejemplo (2): montando un servidor Express
    8 minutos
  49. 49
    Ejemplo (3): haciendo las funciones de control de datos
    11 minutos
  50. 50
    Ejemplo (4): conectando todas las piezas
    7 minutos