Tipado y duck typing en JavaScript

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
Una función que llama a cosa.ladrar() no le pide a cosa ningún certificado de qué clase es. Le alcanza con que, en el momento de la llamada, cosa tenga un método que se llame ladrar. Eso es duck typing, y es el sistema de tipos real que usa JavaScript en tiempo de ejecución.
Ninguna variable tiene un tipo fijo: el tipo lo tiene el valor que guarda en cada momento, y ese valor puede cambiar.
let dato = 4;
console.log(typeof dato); // number
dato = "cuatro";
console.log(typeof dato); // string
dato = true;
console.log(typeof dato); // booleanLa misma variable pasa de number a string a boolean sin que el motor se queje en ningún momento. Esto es tipado dinámico: el chequeo de tipos ocurre en tiempo de ejecución, sobre el valor que hay en ese instante, no en tiempo de compilación sobre una declaración fija. En un lenguaje de tipado estático, declarar una variable como número e intentar guardarle después una cadena es un error que el compilador detecta antes de correr una sola línea.
Esto no dice todavía nada sobre duck typing en sí: solo establece la base. Que el tipo de un valor pueda cambiar es una propiedad de las variables. Duck typing es una propiedad de cómo se deciden las operaciones sobre los valores, y aparece recién cuando hay más de un objeto de por medio.
Una forma de chequear si un objeto "sirve" para algo es preguntarle si es instancia de una clase concreta.
class Perro {
ladrar() {
return "Guau";
}
}
class RobotLadrador {
ladrar() {
return "Guau (sintetizado)";
}
}
const firulais = new Perro();
const r2 = new RobotLadrador();
console.log(firulais instanceof Perro); // true
console.log(r2 instanceof Perro); // false
console.log(firulais.ladrar()); // Guau
console.log(r2.ladrar()); // Guau (sintetizado)Perro y RobotLadrador no comparten ninguna relación de herencia: son dos clases independientes que, por pura coincidencia, definen el mismo método con el mismo nombre. instanceof recorre la cadena de prototipos de r2 buscando el prototipo de Perro en algún escalón, y no lo encuentra, así que devuelve false aunque r2.ladrar() funcione exactamente igual que firulais.ladrar().
Ese false no es un error del motor: es lo que instanceof está diseñado para contestar. Necesita una relación explícita en la cadena de prototipos (una que extends arma automáticamente, o que Object.create/Object.setPrototypeOf arman a mano) para decir que sí. Que los dos objetos tengan el mismo método, con el mismo comportamiento, no le importa nada.
Duck typing invierte la pregunta: en vez de preguntar de qué clase es un objeto, se lo usa directamente y se confía en que responda.
class Perro {
ladrar() {
return "Guau";
}
}
class RobotLadrador {
ladrar() {
return "Guau (sintetizado)";
}
}
function hacerLadrar(cosa) {
console.log(cosa.ladrar());
}
hacerLadrar(new Perro()); // Guau
hacerLadrar(new RobotLadrador()); // Guau (sintetizado)
hacerLadrar({ ladrar: () => "Guau (objeto literal)" }); // Guau (objeto literal)hacerLadrar no le pregunta a cosa de qué clase es, ni siquiera si es instancia de algo: llama a cosa.ladrar() y listo. Le funciona a un Perro, a un RobotLadrador sin ninguna relación con Perro, y a un objeto literal armado en el momento que ni siquiera pasó por una clase. Los tres "caminan como pato" en el único sentido que le importa a esta función: los tres tienen un método ladrar que se puede llamar. El nombre viene de ahí, "si camina como un pato y grazna como un pato, es un pato": no hace falta que declare por escrito que es un pato.
Este comportamiento tiene un costo. Si el objeto que llega no tiene el método, JavaScript no lo avisa antes de tiempo.
function hacerLadrar(cosa) {
console.log(cosa.ladrar());
}
const gato = { maullar: () => "Miau" };
hacerLadrar(gato);
// TypeError: cosa.ladrar is not a functionEl error aparece recién en el momento exacto de la llamada, no antes: es el precio de no chequear ningún tipo declarado de antemano. gato es un objeto perfectamente válido, solo que no tiene la forma que hacerLadrar necesita.
Cuando conviene evitar ese TypeError, la salida no es volver a instanceof: es preguntar directamente si el método existe, antes de llamarlo.
function puedeLadrar(cosa) {
return typeof cosa.ladrar === "function";
}
const cosas = [
{ nombre: "Firulais", ladrar: () => "Guau" },
{ nombre: "Michi", maullar: () => "Miau" },
{ nombre: "R2", ladrar: () => "Guau (sintetizado)" },
];
const sonidos = cosas.filter(puedeLadrar).map((cosa) => cosa.ladrar());
console.log(sonidos); // [ 'Guau', 'Guau (sintetizado)' ]puedeLadrar no le pregunta a cosa qué clase la construyó: usa typeof para confirmar que la propiedad ladrar es, en este objeto puntual, una función llamable. cosas mezcla tres objetos sin ninguna relación entre sí (ni herencia, ni un origen común), y filter se queda solo con los que responden a la forma que hace falta, antes de que map los llame. Esto es duck typing hecho explícito: en vez de confiar a ciegas y capturar el error después, se verifica la forma concreta, método por método, en el momento en que hace falta.
JavaScript no le pregunta a un valor qué tipo declaró tener, ni de qué clase heredó: en el momento de usarlo, revisa si tiene la forma concreta que esa operación necesita.
instanceofpuede confirmar una relación de herencia explícita, pero duck typing no la necesita para nada, y por eso es el mecanismo que decide, en la práctica, casi todas las llamadas de JavaScript.
El post siguiente retoma el comportamiento de valor y referencia ya visto para preguntar qué efectos deja una función además de su resultado, y qué significa exactamente que una función sea pura.