TypeScript

TypeScript

Tutorial TypeScript: Polimorfismo

TypeScript polimorfismo: implementación y ejemplos. Aprende a implementar polimorfismo en TypeScript con ejemplos prácticos y detallados.

Aprende TypeScript y certifícate

Concepto de polimorfismo en TypeScript

El polimorfismo es uno de los pilares fundamentales de la programación orientada a objetos. La palabra proviene del griego y significa "muchas formas", lo que refleja perfectamente su esencia: permitir que objetos de diferentes clases respondan al mismo mensaje o método de maneras distintas.

En TypeScript, el polimorfismo nos permite tratar objetos de clases diferentes a través de una interfaz común, lo que hace nuestro código más flexible, mantenible y extensible. Esta característica es especialmente útil cuando queremos crear sistemas que puedan adaptarse fácilmente a nuevos requisitos sin necesidad de modificar el código existente.

El polimorfismo está estrechamente relacionado con otros principios de la programación orientada a objetos que hemos estudiado, como la herencia y las interfaces, y nos ayuda a aplicar principios como el de "abierto/cerrado" (abierto para extensión, cerrado para modificación).

Formas básicas de polimorfismo

TypeScript soporta principalmente dos tipos de polimorfismo:

  • Polimorfismo por herencia: Cuando las clases derivadas sobrescriben métodos de su clase base
  • Polimorfismo por interfaces: Cuando diferentes clases implementan la misma interfaz

Vamos a explorar cada uno de estos enfoques con ejemplos detallados.

Polimorfismo por herencia

La forma más común de polimorfismo ocurre cuando una clase hija sobrescribe un método de su clase padre, proporcionando una implementación específica mientras mantiene la misma firma del método.

Veamos un ejemplo completo:

// Clase base abstracta que define un "contrato" para las formas
abstract class Shape {
  // La propiedad color es accesible a todas las clases derivadas
  constructor(protected color: string) {}
  
  // Método abstracto - debe implementarse en las clases hijas
  abstract calculateArea(): number;
  
  // Método concreto que usa el método abstracto
  displayInfo(): void {
    console.log(`Forma de color ${this.color} con área de ${this.calculateArea()} unidades cuadradas`);
  }
}

// Clase derivada para círculos
class Circle extends Shape {
  // Agregamos propiedades específicas de Circle
  constructor(
    color: string,
    private radius: number
  ) {
    // Llamamos al constructor de la clase base
    super(color);
  }
  
  // Implementamos el método abstracto con la fórmula específica para círculos
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

// Clase derivada para rectángulos
class Rectangle extends Shape {
  constructor(
    color: string,
    private width: number,
    private height: number
  ) {
    super(color);
  }
  
  // Implementación específica para rectángulos
  calculateArea(): number {
    return this.width * this.height;
  }
}

// Función que demuestra el polimorfismo - trabaja con cualquier Shape
function printShapeInfo(shape: Shape): void {
  // No necesitamos saber qué tipo específico de Shape es
  shape.displayInfo();
}

// Creación de objetos
const circle = new Circle("rojo", 5);
const rectangle = new Rectangle("azul", 4, 6);

// Llamadas polimórficas - mismo método, diferentes implementaciones
printShapeInfo(circle);     // "Forma de color rojo con área de 78.54 unidades cuadradas"
printShapeInfo(rectangle);  // "Forma de color azul con área de 24 unidades cuadradas"

En este ejemplo:

  • Shape define un contrato con el método abstracto calculateArea()
  • Cada clase derivada implementa este método de forma diferente
  • La función printShapeInfo() trabaja con cualquier objeto que sea un Shape
  • En tiempo de ejecución, se llama a la implementación correcta según el tipo real del objeto

Relación con la herencia

Como aprendimos en la lección de herencia, las clases derivadas heredan propiedades y métodos de su clase base. El polimorfismo extiende esta idea permitiendo que las clases derivadas proporcionen su propia implementación de los métodos heredados.

La clase Shape utiliza el modificador protected para la propiedad color, lo que permite que las clases derivadas accedan a ella, demostrando cómo los conceptos de encapsulación y modificadores de acceso que estudiamos anteriormente se relacionan con el polimorfismo.

Polimorfismo mediante interfaces

Otra forma poderosa de implementar polimorfismo es mediante interfaces. A diferencia de la herencia, las interfaces permiten que clases sin relación jerárquica entre sí puedan ser tratadas de manera uniforme.

// Definición de una interfaz - un contrato que las clases deben cumplir
interface Notificable {
  send(message: string): void;
  getChannel(): string;
}

// Implementación para Email
class EmailNotification implements Notificable {
  constructor(private email: string) {}
  
  // Implementación específica para email
  send(message: string): void {
    console.log(`Enviando email a ${this.email}: ${message}`);
    // Aquí iría el código real para enviar el email
  }
  
  getChannel(): string {
    return `Email: ${this.email}`;
  }
}

// Implementación para SMS
class SMSNotification implements Notificable {
  constructor(private phoneNumber: string) {}
  
  // Implementación específica para SMS
  send(message: string): void {
    console.log(`Enviando SMS a ${this.phoneNumber}: ${message}`);
    // Aquí iría el código real para enviar el SMS
  }
  
  getChannel(): string {
    return `SMS: ${this.phoneNumber}`;
  }
}

// Función que utiliza el polimorfismo por interfaz
function sendNotification(notification: Notificable, message: string): void {
  console.log(`Usando canal: ${notification.getChannel()}`);
  notification.send(message);
}

// Uso del sistema
const emailNotif = new EmailNotification("usuario@ejemplo.com");
const smsNotif = new SMSNotification("+34612345678");

// Llamadas polimórficas - misma función, diferentes implementaciones
sendNotification(emailNotif, "Tu pedido ha sido procesado");
sendNotification(smsNotif, "Tu código de verificación es 1234");

Ventajas del polimorfismo mediante interfaces

A diferencia del polimorfismo por herencia, el polimorfismo mediante interfaces ofrece mayor flexibilidad:

  1. No requiere relación jerárquica: Las clases no necesitan compartir una clase base común.
  2. Implementación múltiple: Una clase puede implementar varias interfaces.
  3. Desacoplamiento: Crea un acoplamiento más débil entre los componentes del sistema.

Relación con la lección de interfaces

En la lección anterior sobre interfaces, aprendimos que estas definen un contrato que especifica los métodos y propiedades que una clase debe implementar. El polimorfismo aprovecha este mecanismo para permitir que diferentes objetos sean tratados de manera uniforme a través de su interfaz común, independientemente de sus implementaciones específicas.

Interfaces vs clases abstractas

Aunque tanto las interfaces como las clases abstractas pueden utilizarse para implementar polimorfismo, existen diferencias importantes:

// Usando clase abstracta
abstract class MediaPlayer {
  abstract play(): void;
  abstract pause(): void;
  abstract stop(): void;
  
  // Comportamiento compartido
  displayStatus(status: string): void {
    console.log(`Player status: ${status}`);
  }
}

// Usando interfaz
interface MediaPlayerInterface {
  play(): void;
  pause(): void;
  stop(): void;
}

Las clases abstractas pueden proporcionar implementaciones parciales y mantener estado, mientras que las interfaces solo definen contratos sin implementación. La elección entre una u otra depende de las necesidades específicas:

  • Usa interfaces cuando necesites máxima flexibilidad y no requieras compartir implementación.
  • Usa clases abstractas cuando quieras proporcionar una base común con funcionalidad compartida.

Implementación de múltiples interfaces

Una de las mayores ventajas del polimorfismo con interfaces es la capacidad de implementar múltiples contratos:

interface Playable {
  play(): void;
  pause(): void;
}

interface Recordable {
  startRecording(): void;
  stopRecording(): void;
}

class MediaRecorder implements Playable, Recordable {
  private isRecording: boolean = false;
  
  play(): void {
    console.log("Playing recorded content");
  }
  
  pause(): void {
    console.log("Playback paused");
  }
  
  startRecording(): void {
    this.isRecording = true;
    console.log("Recording started");
  }
  
  stopRecording(): void {
    this.isRecording = false;
    console.log("Recording stopped");
  }
}

Esta capacidad permite crear clases muy flexibles que pueden ser utilizadas en diferentes contextos:

function playContent(player: Playable): void {
  player.play();
}

function recordSession(recorder: Recordable): void {
  recorder.startRecording();
  setTimeout(() => recorder.stopRecording(), 5000);
}

const mediaDevice = new MediaRecorder();
playContent(mediaDevice);    // Usa mediaDevice como Playable
recordSession(mediaDevice);  // Usa mediaDevice como Recordable

Extensión de interfaces

Las interfaces en TypeScript pueden extenderse entre sí, lo que permite crear contratos más especializados:

interface BasicPlayer {
  play(): void;
  pause(): void;
}

interface AdvancedPlayer extends BasicPlayer {
  stop(): void;
  skipForward(): void;
  skipBackward(): void;
}

class PremiumMediaPlayer implements AdvancedPlayer {
  play(): void {
    console.log("Playing media");
  }
  
  pause(): void {
    console.log("Media paused");
  }
  
  stop(): void {
    console.log("Playback stopped");
  }
  
  skipForward(): void {
    console.log("Skipping forward 10 seconds");
  }
  
  skipBackward(): void {
    console.log("Skipping backward 10 seconds");
  }
}

Esto permite un polimorfismo en capas, donde un objeto puede ser tratado a diferentes niveles de abstracción:

function basicPlayback(player: BasicPlayer): void {
  player.play();
  setTimeout(() => player.pause(), 3000);
}

function advancedPlayback(player: AdvancedPlayer): void {
  player.play();
  setTimeout(() => player.skipForward(), 2000);
  setTimeout(() => player.stop(), 5000);
}

const premiumPlayer = new PremiumMediaPlayer();
basicPlayback(premiumPlayer);    // Funciona porque implementa BasicPlayer
advancedPlayback(premiumPlayer); // Funciona porque implementa AdvancedPlayer

Aplicaciones prácticas y beneficios del polimorfismo

El polimorfismo tiene numerosas aplicaciones prácticas que hacen nuestro código más flexible y mantenible. Veamos un ejemplo más completo que demuestra cómo se puede aplicar en un escenario real.

// Interfaz para productos
interface Product {
  name: string;
  price: number;
  getDescription(): string;
  calculateDiscount(): number;
}

// Producto físico
class PhysicalProduct implements Product {
  constructor(
    public name: string,
    public price: number,
    private weight: number
  ) {}
  
  getDescription(): string {
    return `${this.name} - $${this.price} (${this.weight}kg)`;
  }
  
  calculateDiscount(): number {
    // Los productos pesados tienen más descuento
    return this.weight > 10 ? this.price * 0.1 : this.price * 0.05;
  }
  
  // Método específico para productos físicos
  getShippingCost(): number {
    return this.weight * 2;
  }
}

// Producto digital
class DigitalProduct implements Product {
  constructor(
    public name: string,
    public price: number
  ) {}
  
  getDescription(): string {
    return `${this.name} - $${this.price} (Digital)`;
  }
  
  calculateDiscount(): number {
    // Descuento fijo para productos digitales
    return this.price * 0.15;
  }
  
  // Método específico para productos digitales
  getDownloadLink(): string {
    return `https://example.com/download/${this.name.toLowerCase()}`;
  }
}

// Servicio
class Service implements Product {
  constructor(
    public name: string,
    public price: number,
    private hours: number
  ) {}
  
  getDescription(): string {
    return `${this.name} - $${this.price} (${this.hours} horas)`;
  }
  
  calculateDiscount(): number {
    // Descuento por volumen de horas
    return this.hours > 5 ? this.price * 0.12 : this.price * 0.05;
  }
  
  // Método específico para servicios
  getTimeEstimation(): string {
    return `${this.hours} horas aproximadamente`;
  }
}

// Sistema de carrito que usa polimorfismo
class ShoppingCart {
  private items: Product[] = [];
  
  addProduct(product: Product): void {
    this.items.push(product);
  }
  
  // Calcular total con descuentos
  calculateTotal(): number {
    let total = 0;
    
    // Iteramos por los productos de forma uniforme
    for (const item of this.items) {
      const discount = item.calculateDiscount();
      total += item.price - discount;
    }
    
    return total;
  }
  
  // Mostrar resumen del carrito
  displaySummary(): void {
    console.log("===== Resumen del carrito =====");
    
    // Podemos trabajar con todos los productos independientemente de su tipo
    this.items.forEach(item => {
      console.log(item.getDescription());
      const discount = item.calculateDiscount();
      console.log(`  Descuento: $${discount.toFixed(2)}`);
      console.log(`  Precio final: $${(item.price - discount).toFixed(2)}`);
    });
    
    console.log("-----------------------------");
    console.log(`Total: $${this.calculateTotal().toFixed(2)}`);
    console.log("=============================");
  }
}

// Ejemplo de uso del sistema
const cart = new ShoppingCart();

// Agregamos diferentes tipos de productos
cart.addProduct(new PhysicalProduct("Laptop", 1200, 2.5));
cart.addProduct(new DigitalProduct("eBook", 20));
cart.addProduct(new Service("Consultoría", 150, 3));
cart.addProduct(new PhysicalProduct("Mueble", 500, 15));

// Mostramos el resumen usando polimorfismo
cart.displaySummary();

// Acceso a métodos específicos cuando conocemos el tipo concreto
const book = new PhysicalProduct("Libro", 25, 0.5);
console.log(`Costo de envío: $${book.getShippingCost()}`);

const software = new DigitalProduct("Software", 50);
console.log(`Link de descarga: ${software.getDownloadLink()}`);

Beneficios del polimorfismo

El ejemplo anterior demuestra varios beneficios clave del polimorfismo:

  1. Código más limpio y modular: El método calculateTotal() trata a todos los productos de manera uniforme, sin necesidad de verificar su tipo.
  2. Extensibilidad: Podemos añadir nuevos tipos de productos (como SubscriptionProduct) sin modificar el código de ShoppingCart.
  3. Reutilización de código: La lógica común se implementa una sola vez, mientras que cada clase proporciona sus implementaciones específicas.
  4. Mantenibilidad: Los cambios en un tipo no afectan a otros tipos o al código que los utiliza.

Polimorfismo en colecciones

Como vimos en el ejemplo, podemos almacenar diferentes tipos de objetos en un mismo array, siempre que compartan una interfaz común:

// Array heterogéneo de productos
const products: Product[] = [
  new PhysicalProduct("Monitor", 300, 5),
  new DigitalProduct("Antivirus", 40),
  new Service("Instalación", 80, 2)
];

// Operamos sobre todos los productos de manera uniforme
let totalDiscount = 0;
products.forEach(product => {
  totalDiscount += product.calculateDiscount();
  console.log(`${product.name}: ${product.getDescription()}`);
});	

Relación con modificadores de acceso

El polimorfismo trabaja bien con los modificadores de acceso que estudiamos anteriormente:

  • Los métodos sobrescritos deben mantener o ampliar la visibilidad del método original
  • Las propiedades/métodos private no son accesibles desde clases heredadas
  • Las propiedades/métodos protected son accesibles desde clases heredadas pero no desde fuera
  • Las propiedades/métodos public definen la interfaz pública que puede usarse de forma polimórfica

Conclusión

El polimorfismo es una técnica poderosa que permite que objetos de diferentes clases sean tratados de manera uniforme a través de una interfaz común, ya sea mediante herencia o implementación de interfaces.

Como hemos visto, el polimorfismo se relaciona estrechamente con otros conceptos fundamentales:

  • Utiliza la herencia para compartir comportamiento entre clases relacionadas
  • Aprovecha las interfaces para definir contratos comunes entre clases no relacionadas
  • Respeta los modificadores de acceso para controlar la visibilidad y encapsulación

Al emplear el polimorfismo adecuadamente, podemos crear sistemas más flexibles, extensibles y mantenibles, que pueden adaptarse fácilmente a nuevos requisitos sin necesidad de modificar el código existente.

En próximas lecciones, exploraremos cómo combinar estos conceptos en aplicaciones más complejas y cómo aplicar patrones de diseño que aprovechan el poder del polimorfismo.

Aprende TypeScript online

Otros ejercicios de programación de TypeScript

Evalúa tus conocimientos de esta lección Polimorfismo con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Proyecto Inventario de productos

TypeScript
Proyecto

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

Todas las lecciones de TypeScript

Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Typescript

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras Y Efectos Secundarios

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales Y Tipos Condicionales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos Básicos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad (Partial, Required, Pick, Etc)

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el concepto de polimorfismo como la capacidad de objetos de diferentes clases para responder al mismo mensaje de distintas maneras.
  • Implementar polimorfismo mediante herencia, creando clases derivadas que sobrescriban métodos de la clase base.
  • Aplicar polimorfismo mediante interfaces, permitiendo que clases no relacionadas sean tratadas de manera uniforme.
  • Utilizar arrays y parámetros polimórficos para procesar objetos de distintos tipos a través de una interfaz común.
  • Reconocer cómo el polimorfismo mejora la extensibilidad y mantenibilidad del código al reducir dependencias y evitar condicionales complejos.