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ícateConcepto 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 abstractocalculateArea()
- Cada clase derivada implementa este método de forma diferente
- La función
printShapeInfo()
trabaja con cualquier objeto que sea unShape
- 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:
- No requiere relación jerárquica: Las clases no necesitan compartir una clase base común.
- Implementación múltiple: Una clase puede implementar varias interfaces.
- 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:
- 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. - Extensibilidad: Podemos añadir nuevos tipos de productos (como
SubscriptionProduct
) sin modificar el código deShoppingCart
. - Reutilización de código: La lógica común se implementa una sola vez, mientras que cada clase proporciona sus implementaciones específicas.
- 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.
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
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Proyecto Inventario de productos
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
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
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras Y Efectos Secundarios
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales Y Tipos Condicionales
Tipos Intermedios Y Avanzados
Tipos Genéricos Básicos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad (Partial, Required, Pick, Etc)
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
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.