Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
Dando clase esta semana pedí que alguien nombrara los tipos primitivos de JavaScript. Salieron cuatro. Son siete, y con uno de ellos el propio lenguaje miente cuando le preguntás.
Siete tipos, y typeof no siempre ayuda
primitivos.js
typeof "hola"; // "string"typeof 42; // "number"typeof true; // "boolean"typeof undefined; // "undefined"typeof 10n; // "bigint"typeof Symbol("id"); // "symbol"typeof null; // "object" ← este es el que miente
Un primitivo es un valor que no es un objeto: no tiene propiedades propias, se compara por su valor y no por una referencia en memoria, y viene definido por el lenguaje mismo, no por vos. En JavaScript hay siete: string, number, boolean, undefined, bigint, symbol y null. El bloque de arriba les pregunta el tipo a los siete, uno por uno, con typeof.
Seis de siete responden lo que uno espera. null es un primitivo, no un objeto, y sin embargo typeof devuelve "object".
No es un error tuyo ni una sutileza conceptual: es un bug de la primera versión de JavaScript, de 1995, que nunca se arregló. En aquella implementación los valores llevaban una etiqueta de tipo en los bits bajos, y la etiqueta del objeto era 000. El valor null era el puntero nulo, o sea todo ceros. Al preguntarle el tipo, coincidía con la de objeto.
Se propuso corregirlo y se descartó: había demasiado código en el mundo dependiendo de la respuesta equivocada. Si querés distinguir de verdad, hay que ir por otro lado.
detectar-null.js
Object.prototype.toString.call(null); // "[object Null]"Object.prototype.toString.call([]); // "[object Array]"// la forma corta y honestaconst esNulo = (v) => v === null;
Y ya que estamos, typeof tampoco distingue arrays de objetos, porque los arrays son objetos:
typeof-objetos.js
typeof {}; // "object"typeof []; // "object"typeof function () {}; // "function" ← la excepción rara
Un primitivo no se puede modificar
Esta es la propiedad que define a un primitivo, y la que menos se explica.
inmutable.js
let saludo = "hola";saludo[0] = "H";console.log(saludo); // "hola" ← no cambió nada
string es el tipo para texto: cualquier secuencia de caracteres entre comillas simples, dobles, o backticks (que además permiten interpolar valores con ${...}). Dos strings son iguales si tienen el mismo contenido — no hace falta que sean el mismo objeto en memoria.
No falló la asignación por un problema de sintaxis: los strings son inmutables. Y acá aparece la primera particularidad seria del lenguaje, porque qué pasa exactamente depende del modo en el que corra el código:
modos.js
"use strict";let texto = "hola";texto[0] = "H";// TypeError: Cannot assign to read only property '0' of string 'hola'
sin-strict.js
// El mismo código en un script suelto, sin "use strict"var texto = "hola";texto[0] = "H";console.log(texto); // "hola" ← falla en silencio, sin avisar nada
Los módulos ES son siempre strict. Así que el mismo código que en tu proyecto de Vite tira un error, pegado en la consola del navegador no dice absolutamente nada.
Cuando algo parece modificar un string, lo que hace es devolver uno nuevo:
metodos-string.js
const original = "hola";console.log(original.toUpperCase()); // "HOLA"console.log(original); // "hola" ← intacto
Entonces, ¿cómo tiene métodos algo que no es un objeto?
Esta pregunta me la hicieron en clase y es la mejor del tema. Si "hola" es un primitivo, y los primitivos no tienen propiedades, ¿de dónde sale .toUpperCase()?
autoboxing.js
const s = "hola";s.length; // 4s.toUpperCase(); // "HOLA"
Lo que pasa es que el motor, al ver un punto después de un primitivo, envuelve el valor en un objeto temporal, busca el método ahí, lo ejecuta y tira el objeto. Se llama autoboxing, y es invisible salvo cuando se lo agarra en falta:
autoboxing-falla.js
const s = "hola";s.propiedadNueva = "algo";console.log(s.propiedadNueva); // undefined
La asignación funcionó — sobre el objeto temporal. Ese objeto se descartó una línea después, con la propiedad adentro. En strict, directamente lanza TypeError.
Los objetos envoltorio existen y se pueden crear a mano, pero no son lo mismo que el primitivo:
wrappers.js
typeof new String("a"); // "object"new String("a") === "a"; // false ← distinto tiponew String("a") == "a"; // true ← con coerción, sí
Es la razón por la que nunca hay que usar new String, new Number ni new Boolean. Salvo para entender esto.
boolean: dos valores, y todo lo demás se convierte a uno de ellos
boolean.js
Boolean(0); // falseBoolean(""); // falseBoolean(null); // falseBoolean(undefined); // falseBoolean(NaN); // falseBoolean("0"); // true ← string no vacíoBoolean([]); // true ← array vacío, pero es un objetoBoolean({}); // true ← ídem
boolean tiene solo dos valores posibles, true y false. Pero en cualquier lugar donde JavaScript espera uno de los dos —un if, un while, el operador &&— convierte lo que le pongas. Los valores que se convierten a false (los "falsy") son ocho: 0, -0, "", null, undefined, NaN, 0n y el propio false. Todo lo demás se convierte a true, incluidos un array vacío o un objeto vacío.
Y como con string, el objeto envoltorio hace trampa:
boolean-wrapper.js
if (new Boolean(false)) { console.log("esto se ejecuta"); // se ejecuta}
new Boolean(false) es un objeto, y los objetos son siempre truthy sin importar qué haya adentro.
Un solo tipo para todos los números
JavaScript no tiene enteros y decimales por separado. Tiene number, y son todos flotantes de doble precisión, el formato IEEE 754. De ahí sale el ejemplo más citado del lenguaje:
No es un bug de JavaScript: pasa igual en Java, en Python y en C. Es que 0.1 no tiene representación exacta en binario, igual que un tercio no la tiene en decimal. El error aparece al sumar.
Pasado ese valor, los enteros dejan de tener representación única y dos números diferentes empiezan a compartir el mismo lugar. Es exactamente el tipo de cosa que aparece cuando un backend manda un id de 64 bits.
Y hay valores que tienen el tipo number sin representar una cantidad:
infinitos.js
1 / 0; // Infinity ← se pasó del número más grande representable-1 / 0; // -Infinity0 === -0; // true ← para JS son "iguales"Object.is(0, -0); // false ← pero no son el mismo valor1 / -0; // -Infinity ← esto es lo que los delata
Infinity no cuenta nada: es lo que devuelve el lenguaje cuando un cálculo se escapa del rango que number puede representar, como dividir por cero. Y el cero tiene dos firmas internas, 0 y -0 — para casi cualquier comparación se comportan igual, === los iguala — pero no son el mismo valor: dividir por cada uno da un infinito de signo distinto, y eso es justo lo que detecta Object.is.
NaN es un número que no es igual a sí mismo
nan.js
0 / 0; // NaN ← una cuenta imposibleMath.sqrt(-1); // NaN ← otra cuenta imposible, distintatypeof NaN; // "number"NaN === NaN; // false
NaN significa "Not a Number", y es lo que devuelve el lenguaje cuando le pedís una cuenta que no tiene resultado numérico válido. Su tipo es number —es el único valor numérico inválido que existe— y aun así no es igual a sí mismo. Eso viene del estándar IEEE 754, no es una decisión de JavaScript: 0 / 0 y Math.sqrt(-1) son dos cuentas distintas, las dos inválidas, y el estándar no da por sentado que dos resultados inválidos sean "el mismo resultado". Por eso NaN nunca es igual a nada, ni siquiera a otro NaN.
La consecuencia práctica es que no lo podés buscar con comparación:
detectar-nan.js
Number.isNaN(NaN); // true ← la forma correctaObject.is(NaN, NaN); // true ← también sirve[NaN].includes(NaN); // true ← includes usa Object.is[NaN].indexOf(NaN); // -1 ← indexOf usa ===, no lo encuentra
Ese último par es de mis favoritos: dos métodos del mismo array, buscando el mismo valor, con respuestas distintas. includes llegó en ES2016 con una comparación que contempla NaN; indexOf es de ES5 y usa ===.
Se declara con una n al final o con BigInt(). Resuelve el problema de los enteros grandes, pero con una regla estricta: no se mezcla con number.
bigint-mezcla.js
10n + 1;// TypeError: Cannot mix BigInt and other types, use explicit conversions
Es de las pocas veces que JavaScript se niega a hacer una coerción en vez de inventar un resultado. Y tiene sentido: convertir en silencio arruinaría la precisión que es la razón de existir del tipo.
Symbol: una llave que no choca con ninguna otra
symbol.js
const a = Symbol("id");const b = Symbol("id");typeof a; // "symbol"a === b; // false ← la descripción es solo una etiquetaa.description; // "id"
symbol es el primitivo más joven de los siete —llegó en ES2015— y tiene un solo trabajo: generar un valor que el motor garantiza único. No representa texto ni una cantidad; cada Symbol(...) que se crea es, por definición, distinto de cualquier otro que exista, incluso si a los dos les pasás la misma descripción. Esa descripción ("id" en el ejemplo) es solo una etiqueta para leer en el debugger — no participa en la igualdad ni identifica nada.
Por eso sirven como claves de propiedad que no pueden colisionar con las de otro, y que además quedan fuera de los recorridos normales:
La propiedad existe y se puede leer, pero no aparece en Object.keys ni se serializa. Es el tipo que menos se usa a mano y más se usa sin saberlo: Symbol.iterator es lo que hace que un array se pueda recorrer con for...of.
null y undefined no son lo mismo
null-undefined.js
null === undefined; // false ← distinto tiponull == undefined; // true ← el único par que == iguala así
undefined es lo que el lenguaje pone cuando algo no tiene valor: una variable declarada sin asignar, un parámetro que no llegó, una propiedad que no existe. null es lo que vos ponés para decir "acá no hay nada, a propósito".
La diferencia es de intención, y se nota al leer el código de otro: un undefined suele ser un descuido; un null es una decisión.
Aunque null también tiene su rareza propia:
null-raro.js
null == 0; // falsenull >= 0; // true ← ¿?
Los operadores de comparación y los de igualdad usan reglas distintas de conversión. >= convierte null a 0 y compara; == tiene una regla especial que solo lo iguala con undefined. Dos caminos diferentes para el mismo valor.
Lo que me llevo
Los primitivos parecen el tema fácil del lenguaje y son donde están enterradas casi todas sus rarezas: un typeof que miente por compatibilidad con 1995, valores inmutables que igual tienen métodos, un solo tipo numérico con dos ceros y un valor que no es igual a sí mismo.
Si estás empezando, quedate con tres cosas: usá Number.isNaN en vez de comparar, no uses new String, y desconfiá de typeof cuando el valor puede ser null.
Y si venís de un lenguaje tipado, la costumbre que más cuesta soltar es asumir que typeof te va a decir la verdad. Te dice lo que decía en 1995.
En la segunda parte vamos por los tipos compuestos: objetos, arrays y funciones. Ahí el tema deja de ser qué es cada valor y pasa a ser dónde vive, por qué copiar no copia y por qué const no protege lo que uno cree.