El Symbol: un valor único garantizado
El tipo Symbol es uno de los tipos primitivos de JavaScript, junto con string, number, boolean, null, undefined y bigint. Su característica principal es muy sencilla: cada símbolo que creas es distinto a cualquier otro, para siempre.
Dos Symbol, ambos creados sin argumentos, y aun así no son iguales. No es un descuido del lenguaje: es justo la idea. No puedes falsificar un Symbol, ni recrearlo por accidente, ni chocar con el de otra persona.
Si quieres, puedes pasarle una descripción para que sea más fácil depurar. Eso no cambia su identidad:
La misma descripción, símbolos distintos. La descripción es simplemente una etiqueta pensada para que los humanos la lean en los logs.
Por qué existen: claves sin colisiones
La razón principal por la que existen los symbols en JavaScript es permitirte añadir propiedades a un objeto sin miedo a que tu clave choque con la de otra persona. Las claves tipo string compiten en un mismo espacio de nombres plano: si dos librerías deciden guardar metadatos en obj.meta, se pisan entre sí. Las claves tipo symbol no compiten, porque nadie más tiene tu symbol.
Los corchetes en [ID]: 42 son la sintaxis de propiedad computada: básicamente le dicen a JavaScript "usa el valor de ID como clave". Solo el código que tenga ese mismo symbol ID podrá leer o sobrescribir esa propiedad. Cualquier otro módulo que use la cadena "id" o su propio Symbol("id") estará apuntando a un espacio completamente distinto.
Las claves tipo symbol están (casi) ocultas
Las propiedades cuya clave es un symbol no aparecen en for...in, ni en Object.keys, ni en JSON.stringify. No son secretas —si alguien se empeña, las puede encontrar—, pero se mantienen apartadas de la iteración habitual.
Object.keys y JSON.stringify ignoran por completo las claves de tipo symbol. Si lo que quieres es encontrar propiedades symbol a propósito, tienes Object.getOwnPropertySymbols y Reflect.ownKeys, que sí las exponen. Este es justo el comportamiento ideal para metadatos: aparecen cuando las buscas, pero quedan ocultas ante cualquier código que solo recorra claves de tipo string.
Compartir symbols con Symbol.for
Symbol() te da un symbol local, único y aislado. A veces necesitas lo contrario: un symbol que sea el mismo en todo el programa, a través de distintos módulos, para que diferentes partes del código puedan ponerse de acuerdo sobre una misma clave. Para eso existe Symbol.for.
Symbol.for(key) consulta un registro global: si ya existe un symbol con esa clave, te lo devuelve; si no, lo crea y lo guarda. Symbol.keyFor hace lo contrario: te devuelve la clave con la que se registró un symbol (o undefined si no está registrado).
La mayoría de las veces te basta con Symbol() a secas. Usa Symbol.for solo cuando necesites de verdad compartir un symbol entre distintos módulos.
Well-known symbols: engancharte al propio lenguaje
Aquí es donde los symbols se ganan el sueldo. JavaScript expone un conjunto de symbols predefinidos —los llamados well-known symbols— que el propio lenguaje busca en tus objetos. Si implementas un método usando una de estas claves, tu objeto se conecta directamente a una característica nativa del lenguaje.
El más útil de todos es Symbol.iterator. Cualquier objeto que tenga un método en esa clave es iterable: funciona con for...of, con el spread, con la desestructuración y con Array.from.
range no es un array. Es un objeto común y corriente con un método que usa una clave especial. Pero como ese método devuelve un iterador, tanto for...of como el spread funcionan sobre él, igual que funcionan con arrays, strings o Map. Ese es el contrato: implementa Symbol.iterator y el lenguaje te considera iterable.
Otros well-known symbols que vale la pena conocer
Hay varios más, y cada uno es un gancho hacia una parte distinta del lenguaje:
Symbol.iterator: sirve para hacer un objeto iterable en JavaScript.Symbol.asyncIterator: lo mismo, pero para usarlo confor await...of.Symbol.toPrimitive: controla cómo un objeto se convierte a un valor primitivo (número, string o el modo por defecto) cuando ocurre una coerción.Symbol.hasInstance: personaliza el resultado deinstanceofpara tu clase.Symbol.toStringTag: define la etiqueta que muestraObject.prototype.toString.call(obj).
No hace falta que te los aprendas de memoria. Con saber que existen basta: cuando te topes con un caso en el que quieras que tu objeto se comporte como un tipo nativo, seguramente haya un well-known symbol pensado justo para eso.
Un symbol no es un string
Aquí hay un detalle que pilla a mucha gente: los symbols no se convierten automáticamente a string. Si intentas concatenar uno te lanza un error, y lo mismo pasa si se lo pasas a una API que espera un string:
Los template literals lanzan el mismo TypeError. Si lo que quieres es texto, usa String(symbol) o symbol.toString(). Esas asperezas son intencionales: el lenguaje te protege para que no trates por accidente un valor de identidad única como si fuera un string cualquiera.
Cuándo usar symbols en JavaScript (y cuándo no)
Tira de symbols cuando:
- Estés añadiendo metadatos a objetos que no controlas y necesites una clave que no colisione con nada.
- Estés diseñando un protocolo: "los objetos que quieran funcionar con mi librería deben implementar un método en este symbol".
- Quieras una propiedad que no se cuele en
JSON.stringifyni enfor...in. - Vayas a implementar
Symbol.iteratoru otro well-known symbol.
Olvídate de los symbols cuando:
- Solo necesites una clave de objeto y no haya riesgo de colisión. Un string es más simple y aparece tal cual en los logs.
- Busques campos "privados". Las propiedades con clave de tipo symbol no son realmente privadas:
Object.getOwnPropertySymbolslas encuentra. Para privacidad de verdad, usa campos de clase con#private. - Estés guardando datos que deban sobrevivir a
JSON.stringify. No sobreviven.
La mayoría del código JavaScript se apaña perfectamente sin escribir jamás un Symbol(...) a mano. Pero en cuanto quieras que tu propio objeto funcione con for...of o se comporte de forma natural en un contexto de coerción, los symbols son la puerta de entrada a esa maquinaria.
Siguiente: declaración de funciones
Los symbols, los iteradores y los generadores se apoyan fuertemente en las funciones: los métodos guardados en Symbol.iterator, las factorías que devuelven iteradores, los generadores cuya forma function* esconde casi todo el boilerplate. El próximo capítulo va de funciones, empezando por las distintas maneras de declararlas y qué cambia entre una forma y otra.
Preguntas frecuentes
¿Qué es un symbol en JavaScript?
Un symbol es un tipo primitivo cuyos valores son únicos por definición. Se crea con Symbol() o Symbol('descripción'), y dos llamadas nunca devuelven symbols iguales. Se usan sobre todo como claves de objeto que no colisionan con otras claves, y como puntos de enganche con el propio lenguaje a través de los well-known symbols, como Symbol.iterator.
¿Cuándo conviene usar un symbol en lugar de una clave string?
Cuando quieres añadir una propiedad a un objeto sin arriesgarte a chocar con el nombre de otra propiedad: librerías que guardan metadatos, frameworks que etiquetan objetos o claves que a propósito no forman parte de la API pública. Para claves del día a día, donde prima la legibilidad y no hay riesgo de colisión, lo normal sigue siendo usar strings.
¿Para qué sirve Symbol.iterator?
Symbol.iterator es un well-known symbol que le dice a for...of, al spread y al destructuring cómo recorrer un objeto. Si defines un método en la clave Symbol.iterator que devuelva un iterador, tu objeto pasa a ser iterable. Así es, por debajo, como funcionan los arrays, los strings, los Map y los Set.
¿Qué diferencia hay entre Symbol() y Symbol.for()?
Symbol('x') crea un symbol nuevo cada vez que lo llamas: dos llamadas con la misma descripción siguen siendo symbols distintos. Symbol.for('x'), en cambio, consulta un registro global y devuelve el mismo symbol para la misma clave en todo el programa. Usa Symbol.for cuando necesites compartir el symbol entre módulos o realms, y Symbol() cuando solo busques unicidad local.