Symbols
JavaScript's unique identifier primitive — Symbol(), well-known symbols, Symbol.iterator, Symbol.for, and using symbols as private-ish object keys.
Symbol keys are hidden from Object.keys, for...in and JSON.stringify, which makes them look private. They aren't. Object.getOwnPropertySymbols() and Reflect.ownKeys() list them, and Node's console.log prints them along with their values. Keep an API key under a Symbol and the first debug log of that object puts it in your logs.
const API_KEY = Symbol("apiKey");
const account = { name: "Alice", [API_KEY]: "sk-live-1234" };
console.log(Object.keys(account)); // looks hidden...
console.log(account); // ...until someone logs the object
console.log(Object.getOwnPropertySymbols(account).map(s => account[s]));
// [ 'name' ]
// { name: 'Alice', [Symbol(apiKey)]: 'sk-live-1234' }
// [ 'sk-live-1234' ]Unique keys that never collide
JavaScript objects can have properties keyed by strings or Symbols.
String keys can clash — if two pieces of code both add a "id" property,
one overwrites the other. A Symbol is guaranteed unique — even two Symbols
created with the same description are different values.
Creating Symbols
const sym1 = Symbol("description"); // the description is just for debugging
const sym2 = Symbol("description");
console.log(sym1 === sym2); // false — ALWAYS different
console.log(typeof sym1); // "symbol"
console.log(sym1.description); // "description" — the label you gave it
console.log(sym1.toString()); // "Symbol(description)"
// You cannot new Symbol() — it's not a constructor:
// new Symbol() // TypeError: Symbol is not a constructorUsing Symbols as object keys
const SECRET = Symbol("secret");
const ID = Symbol("id");
const user = {
name: "Alice",
[SECRET]: "password123", // computed property syntax for Symbol keys
[ID]: 42,
};
// Access with the Symbol reference:
console.log(user[SECRET]); // "password123"
console.log(user[ID]); // 42
console.log(user.name); // "Alice"
// Symbol keys are hidden from normal enumeration:
console.log(Object.keys(user)); // ["name"] — no symbols
console.log(JSON.stringify(user)); // {"name":"Alice"} — symbols dropped
// You need the actual Symbol reference to access it:
// user["secret"] → undefined (that's a string key, not the Symbol)Symbols prevent name collisions. They don't control access. When a value must not be readable from outside, use a class private field (#name). It's enforced by the language, it isn't listed by any reflection method, and Node's console.log doesn't print it:
class Account {
#apiKey; // private field: unreachable outside the class
constructor(name, apiKey) {
this.name = name;
this.#apiKey = apiKey;
}
authHeader() { return `Bearer ${this.#apiKey}`; }
}
const account = new Account("Alice", "sk-live-1234");
console.log(account);
console.log(Object.getOwnPropertySymbols(account));
console.log(account.authHeader());
// Account { name: 'Alice' }
// []
// Bearer sk-live-1234The main uses of Symbols
// 1. Unique constants (no collision with other code):
const STATUS_PENDING = Symbol("pending");
const STATUS_DONE = Symbol("done");
const STATUS_FAILED = Symbol("failed");
function process(task) {
task.status = STATUS_PENDING;
// ... do work ...
task.status = STATUS_DONE;
}
// 2. Adding metadata to objects you don't own:
const META = Symbol("meta");
const thirdPartyObject = { data: [1, 2, 3] };
thirdPartyObject[META] = { addedAt: Date.now() }; // won't clash with any existing key
// 3. The global registry — share a Symbol across modules:
const shared1 = Symbol.for("app.user.id");
const shared2 = Symbol.for("app.user.id");
console.log(shared1 === shared2); // true — same symbol from registryOfficial Sources
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.