Polimorfismo en JavaScript: escribir código flexible sin depender de clases
Qué es el polimorfismo
El polimorfismo significa que distintas piezas de código pueden responder a la misma operación, aunque internamente funcionen de manera diferente.
La idea importante no es “usar clases”. La idea importante es poder escribir una función que trabaje con un comportamiento esperado, sin necesitar saber exactamente qué tipo de objeto recibió.
Por ejemplo, si varios objetos tienen un método render(), una función puede llamarlo sin preguntar si el objeto es una tarjeta, una alerta o un modal:
function renderizarComponente(componente) {
return componente.render();
}
Ese es el punto central: el código consumidor depende de una capacidad, no de una clase concreta.
Por qué importa
Sin polimorfismo, el código suele llenarse de condicionales:
function calcularTotal(metodoPago, monto) {
if (metodoPago.tipo === 'tarjeta') {
return monto + monto * 0.03;
}
if (metodoPago.tipo === 'transferencia') {
return monto;
}
if (metodoPago.tipo === 'cripto') {
return monto + monto * 0.01;
}
throw new Error('Método de pago no soportado');
}
Esto funciona, pero escala mal. Cada nuevo método de pago obliga a modificar la misma función. Esa función empieza a conocer demasiados detalles del sistema.
Con polimorfismo, cada método de pago sabe calcular su propio costo:
const pagoConTarjeta = {
calcularTotal(monto) {
return monto + monto * 0.03;
},
};
const pagoPorTransferencia = {
calcularTotal(monto) {
return monto;
},
};
const pagoConCripto = {
calcularTotal(monto) {
return monto + monto * 0.01;
},
};
function cobrar(metodoPago, monto) {
return metodoPago.calcularTotal(monto);
}
console.log(cobrar(pagoConTarjeta, 1000)); // 1030
console.log(cobrar(pagoPorTransferencia, 1000)); // 1000
console.log(cobrar(pagoConCripto, 1000)); // 1010
La función cobrar no necesita saber si el pago es con tarjeta, transferencia o cripto. Solo necesita que el objeto tenga un método calcularTotal.
Polimorfismo con clases
JavaScript permite usar clases, así que también podemos representar polimorfismo con herencia:
class MetodoPago {
calcularTotal() {
throw new Error('El método calcularTotal debe implementarse');
}
}
class Tarjeta extends MetodoPago {
calcularTotal(monto) {
return monto + monto * 0.03;
}
}
class Transferencia extends MetodoPago {
calcularTotal(monto) {
return monto;
}
}
class Cripto extends MetodoPago {
calcularTotal(monto) {
return monto + monto * 0.01;
}
}
function cobrar(metodoPago, monto) {
return metodoPago.calcularTotal(monto);
}
const metodos = [new Tarjeta(), new Transferencia(), new Cripto()];
for (const metodo of metodos) {
console.log(cobrar(metodo, 1000));
}
Este enfoque es útil cuando existe una jerarquía clara. Por ejemplo: distintos tipos de usuarios, distintos proveedores de almacenamiento o distintas estrategias de envío.
El problema aparece cuando se usa herencia solo por costumbre. En JavaScript muchas veces alcanza con objetos simples y funciones.
Duck typing: el estilo natural de JavaScript
JavaScript no exige que un objeto herede de una clase específica para poder usarlo. Si tiene el método que necesitas, puedes usarlo.
A esto se le suele llamar duck typing: si camina como pato y suena como pato, se trata como pato.
const loggerConsola = {
log(mensaje) {
console.log(`[console] ${mensaje}`);
},
};
const loggerArchivo = {
log(mensaje) {
// Simulación de escritura en archivo
return `Guardado: ${mensaje}`;
},
};
function registrarEvento(logger, evento) {
return logger.log(evento);
}
registrarEvento(loggerConsola, 'Usuario registrado');
registrarEvento(loggerArchivo, 'Pago aprobado');
La función registrarEvento no pregunta si el logger viene de una clase Logger. Solo necesita que exista log.
Este patrón aparece muchísimo en JavaScript moderno:
- Componentes que reciben una función
onClick. - Adaptadores que exponen el mismo método
send. - Estrategias que implementan
validate,parse,formatoexecute. - Objetos mock en tests que imitan una dependencia real.
Polimorfismo con funciones
No todo polimorfismo necesita objetos. También puedes conseguirlo pasando funciones con la misma firma.
function aplicarDescuento(precio, calcularDescuento) {
return precio - calcularDescuento(precio);
}
function descuentoVip(precio) {
return precio * 0.2;
}
function descuentoBlackFriday(precio) {
return precio * 0.35;
}
function sinDescuento() {
return 0;
}
console.log(aplicarDescuento(100, descuentoVip)); // 80
console.log(aplicarDescuento(100, descuentoBlackFriday)); // 65
console.log(aplicarDescuento(100, sinDescuento)); // 100
La función aplicarDescuento no conoce las reglas de negocio. Solo recibe una estrategia compatible.
Este estilo es muy común en JavaScript porque las funciones son valores. Puedes pasarlas, guardarlas en objetos, componerlas y reemplazarlas con facilidad.
Un caso más real: validadores
Imagina un formulario donde cada campo tiene una regla diferente:
const requerido = {
validar(valor) {
return valor.trim().length > 0;
},
mensaje: 'El campo es obligatorio',
};
const email = {
validar(valor) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valor);
},
mensaje: 'El email no es válido',
};
const minimoCaracteres = {
validar(valor) {
return valor.length >= 8;
},
mensaje: 'Debe tener al menos 8 caracteres',
};
function validarCampo(valor, validadores) {
return validadores.filter((validador) => !validador.validar(valor)).map((validador) => validador.mensaje);
}
console.log(validarCampo('', [requerido, email]));
console.log(validarCampo('test@mail.com', [requerido, email]));
console.log(validarCampo('123', [requerido, minimoCaracteres]));
Todos los validadores tienen la misma forma: validar(valor) y mensaje. La función validarCampo puede trabajar con cualquier regla nueva mientras respete ese contrato.
Esto permite agregar validadores sin modificar la función principal.
Polimorfismo vs condicionales
No significa que los if sean malos. A veces un condicional simple es más claro.
El polimorfismo empieza a tener sentido cuando:
- Hay varias variantes de un mismo comportamiento.
- Las variantes crecen con el tiempo.
- Cada variante tiene reglas propias.
- La función principal empieza a llenarse de
if,switcho validaciones por tipo. - Quieres testear cada comportamiento por separado.
Si solo tienes dos casos simples, quizá un if es suficiente. Si tienes diez casos que cambian seguido, probablemente conviene separar comportamientos.
Errores comunes
Un error común es creer que polimorfismo significa “crear una clase base para todo”. En JavaScript eso suele producir jerarquías rígidas y difíciles de mantener.
Otro error es no definir bien el contrato. Si una función espera que un objeto tenga calcularTotal(monto), todos los objetos compatibles deberían respetar esa firma. Si uno devuelve número, otro devuelve string y otro lanza errores inesperados, el polimorfismo deja de ayudar.
También hay que evitar abstraer demasiado pronto. El polimorfismo sirve cuando hay variación real. Si todavía no sabes qué partes van a cambiar, una abstracción prematura puede complicar el código.
Conclusión
El polimorfismo en JavaScript consiste en diseñar código alrededor de comportamientos compartidos. Puede lograrse con clases, objetos simples o funciones.
La versión más práctica del concepto es esta: escribe funciones que dependan de lo que algo puede hacer, no de qué clase exacta es.
Cuando se usa bien, el resultado es código más fácil de extender, testear y mantener. Cuando se usa mal, solo agrega capas innecesarias. La clave está en aplicarlo donde realmente hay variantes de comportamiento.