Polimorfismo

Experto
TypeScript
TypeScript
Actualizado: 09/05/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

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

Guarda tu progreso

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

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.

Aprendizajes 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.

Completa TypeScript y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración