Symbols en JavaScript

Intermedio
JavaScript
JavaScript
Actualizado: 19/04/2026

Qué es un Symbol

El tipo Symbol se añadió en ES2015 para cubrir una necesidad muy concreta del lenguaje: disponer de valores únicos e irrepetibles que sirvan como identificadores. A diferencia de un número o una cadena, dos Symbols creados por separado nunca son iguales, aunque compartan la misma descripción.

Este comportamiento lo convierte en la herramienta ideal para definir claves de propiedad que no van a colisionar con otras escritas por terceros, así como para exponer puntos de extensión del lenguaje (los llamados well-known symbols).

const id1 = Symbol("id");
const id2 = Symbol("id");

console.log(id1 === id2); // false
console.log(typeof id1);  // "symbol"

La descripción ("id" en el ejemplo) es puramente informativa: aparece al imprimir el Symbol o en el depurador, pero no afecta a su identidad. Dos Symbols con la misma descripción siguen siendo distintos.

Crear Symbols y acceder a su descripción

El constructor Symbol() se invoca sin new. Llamarlo con new lanza un TypeError, porque los valores Symbol no son objetos.

const userToken = Symbol("user-token");

console.log(userToken.description); // "user-token"
console.log(userToken.toString());  // "Symbol(user-token)"

La propiedad description, añadida en ES2019, permite recuperar la descripción sin llamar a toString(). Es útil para emitir mensajes de error o trazas sin exponer el valor completo.

Al ser un tipo primitivo, los Symbols no se convierten implícitamente a cadena. Si intentas concatenar uno con un string, JavaScript lanza un TypeError:

const s = Symbol("x");

// console.log("Valor: " + s); // TypeError: Cannot convert a Symbol value to a string
console.log(`Valor: ${s.toString()}`); // "Valor: Symbol(x)"

Este comportamiento evita coerciones accidentales. Siempre que necesites representarlo como texto, hazlo de forma explícita.

Symbols como claves de propiedad

El caso de uso más habitual es emplear Symbols como claves de propiedades en objetos. Al ser únicos, evitan colisiones con claves de cadena ya existentes o con las que puedan añadir otros módulos.

const internalId = Symbol("internalId");

const product = {
  name: "Teclado mecánico",
  price: 89.99,
  [internalId]: "sku-0001"
};

console.log(product.name);         // "Teclado mecánico"
console.log(product[internalId]);  // "sku-0001"

Una propiedad indexada por Symbol no aparece en Object.keys, Object.entries, ni en un bucle for...in. Tampoco se serializa con JSON.stringify:

console.log(Object.keys(product));              // ["name", "price"]
console.log(JSON.stringify(product));           // '{"name":"Teclado mecánico","price":89.99}'
console.log(Object.getOwnPropertySymbols(product)); // [Symbol(internalId)]

Para inspeccionar las claves Symbol de un objeto se usan Object.getOwnPropertySymbols o Reflect.ownKeys. Esto hace que sean especialmente adecuadas para metadatos privados: siguen siendo accesibles para quien conoce el Symbol, pero no interfieren con la enumeración normal de propiedades.

Registro global: Symbol.for y Symbol.keyFor

Symbol() crea siempre un valor nuevo. Cuando se necesita un identificador compartido entre módulos o contextos, existe un registro global accesible mediante Symbol.for(key).

const a = Symbol.for("app.userId");
const b = Symbol.for("app.userId");

console.log(a === b); // true

La primera llamada registra el Symbol bajo esa clave; las siguientes devuelven el mismo valor. El camino inverso, obtener la clave de un Symbol registrado, se hace con Symbol.keyFor:

console.log(Symbol.keyFor(a)); // "app.userId"

const local = Symbol("x");
console.log(Symbol.keyFor(local)); // undefined (no está en el registro)

El registro global es útil para compartir identidades entre bundles, workers o iframes sin pasar el valor como referencia. Como contrapartida, los Symbols registrados sí compiten por el espacio de nombres global; por ello conviene usar claves con prefijo (app.*, company.*, etc.) que eviten colisiones.

Well-known symbols: personalizar el comportamiento del lenguaje

JavaScript expone una serie de Symbols conocidos (Symbol.iterator, Symbol.toPrimitive, Symbol.asyncIterator, etc.) que actúan como puntos de extensión del lenguaje. Implementar una propiedad indexada por uno de estos Symbols cambia el comportamiento nativo del objeto.

Symbol.iterator

Permite que un objeto participe en for...of, en la sintaxis de spread y en la desestructuración de arrays:

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const range = new Range(1, 5);

for (const n of range) {
  console.log(n); // 1, 2, 3, 4, 5
}

console.log([...range]); // [1, 2, 3, 4, 5]

Symbol.toPrimitive

Controla cómo se convierte un objeto a un valor primitivo cuando el motor necesita un número, una cadena o un valor por defecto:

class Price {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  [Symbol.toPrimitive](hint) {
    if (hint === "number") return this.amount;
    if (hint === "string") return `${this.amount.toFixed(2)} ${this.currency}`;
    return `${this.amount} ${this.currency}`;
  }
}

const p = new Price(49.5, "EUR");

console.log(+p);          // 49.5         (hint: "number")
console.log(`${p}`);      // "49.50 EUR"  (hint: "string")
console.log(p + "");      // "49.5 EUR"   (hint: "default")

Otros Symbols muy habituales son Symbol.asyncIterator (para iterar con for await...of), Symbol.hasInstance (personalizar instanceof) y Symbol.toStringTag (controlar el valor que devuelve Object.prototype.toString).

class ApiClient {
  get [Symbol.toStringTag]() {
    return "ApiClient";
  }
}

const client = new ApiClient();
console.log(Object.prototype.toString.call(client)); // "[object ApiClient]"

Patrones habituales

Claves "casi privadas" en clases

Antes de la llegada de los campos privados con #, los Symbols eran la forma idiomática de mantener datos fuera de la API pública de un objeto:

const _balance = Symbol("balance");

class Account {
  constructor(initial) {
    this[_balance] = initial;
  }

  deposit(amount) {
    this[_balance] += amount;
  }

  get balance() {
    return this[_balance];
  }
}

const account = new Account(100);
account.deposit(50);

console.log(account.balance);           // 150
console.log(Object.keys(account));      // []  (no aparece la clave Symbol)

Aunque técnicamente no son privados (cualquiera que pueda obtener el Symbol puede leer el valor), quedan fuera de la enumeración habitual y del JSON.

Constantes enumeradas sin colisiones

Para definir conjuntos cerrados de valores (estados, tipos de evento…), los Symbols ofrecen identidad fuerte sin depender de cadenas:

const Status = Object.freeze({
  PENDING:   Symbol("PENDING"),
  APPROVED:  Symbol("APPROVED"),
  REJECTED:  Symbol("REJECTED")
});

function handle(status) {
  switch (status) {
    case Status.PENDING:  return "Procesando…";
    case Status.APPROVED: return "Aprobado";
    case Status.REJECTED: return "Rechazado";
    default:              return "Desconocido";
  }
}

console.log(handle(Status.APPROVED)); // "Aprobado"

No hay forma de pasar accidentalmente una cadena equivocada: el valor tiene que ser uno de los Symbols del enumerado.

Buenas prácticas

  • Usa Symbol() cuando necesites un identificador local y único; usa Symbol.for cuando el valor deba compartirse entre módulos o contextos.
  • Prefiero campos privados (#field) para estado interno de clases en código nuevo; reserva los Symbols para cuando la clave deba ser compartible o compatible con objetos planos.
  • Documenta las claves Symbol que formen parte de una API. Son "invisibles" en las enumeraciones, así que un consumidor no las descubrirá por casualidad.
  • Aprovecha los well-known symbols para integrarte con el lenguaje en lugar de inventar APIs paralelas: un iterable personalizado se comporta como un array en for...of, spread y desestructuración.
  • Evita convertir Symbols a cadena con concatenación; usa String(sym) o sym.description de forma explícita.

Resumen

Los Symbols aportan un tipo primitivo con identidad garantizada: cada Symbol() es único, lo que los convierte en claves de propiedad sin colisiones y en constantes difíciles de falsificar. El registro global (Symbol.for) permite compartir identidades entre módulos, mientras que los well-known symbols abren la puerta a personalizar comportamientos del lenguaje como la iteración o la conversión a primitivo. Usados con criterio, son una herramienta potente para diseñar APIs robustas y poco acopladas.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, JavaScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de JavaScript

Explora más contenido relacionado con JavaScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender qué son los Symbols y por qué garantizan unicidad.
  • Crear y comparar Symbols, entendiendo que cada Symbol es irrepetible.
  • Usar Symbols como claves de propiedad para evitar colisiones en objetos.
  • Compartir Symbols entre módulos con Symbol.for y el registro global.
  • Implementar well-known symbols como Symbol.iterator y Symbol.toPrimitive.
  • Reconocer los casos donde Symbol aporta valor frente a strings o cadenas.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje