Symbol data type
What are symbols?
As we have mentioned earlier, in JavaScript property keys are either strings or symbols. So far we have only used string keys, but what are symbols?
A symbol is a primitive data type, like string or number. It cannot be created by means of a literal.
A symbol can only be created by using the Symbol
wrapper object constructor as a regular function.
const myNumber = Number(2);
console.log(myNumber); // logs: 2
const mySymbol = Symbol();
console.log(typeof mySymbol); // logs: "symbol"
console.log(mySymbol); // logs: Symbol()
BTW. Trying to creating an explicit symbol wrapper object by using Symbol
as a constructor (by using new
),
will throw a TypeError
.
Note that Number(argument)
returns the argument converted to a number, if it is not a number already.
But Symbol()
does not have an argument in the example above. There is no conversion involved at all.
Symbol()
does not return a distinctive value that can be made visible (hence, they cannot be represented in a literal form).
Symbol()
creates a "hidden" guaranteed unique primitive value.
Symbols are particularly used as object property keys, which will be explained in the next section.
You can add an argument to Symbol()
though. It needs to be a string and represents the symbol's description,
but it is not really doing anything. It just makes the symbol more descriptive or identifiable, for instance, during debugging.
The Symbol.description
property returns the read-only description of a symbol.
// symbols are guaranteed to be unique values:
console.log( Symbol('foo') === Symbol('foo') ); // logs: false
const myObj = {};
myObj[Symbol('foo')] = 1;
myObj[Symbol('foo')] = 2;
console.log(myObj); // logs: { Symbol("foo"): 1, Symbol("foo"): 2 }
// get a symbol's description:
const mySymbol = Symbol("some description");
console.log( mySymbol.description ); // logs: "some description"
Symbol type conversions
Symbols never coerce to numbers. Trying to convert a symbol to a number will throw a TypeError
.
// next statements both throw a TypeError:
console.log( +Symbol() );
console.log( Number(Symbol()) );
Symbols do not coerce to strings (throws a TypeError
), but can be explicitly converted to strings
by using String(symbol)
(or symbol.toString()
, but not via new String(symbol)
).
console.log( String(Symbol('foo')) ); // logs: "Symbol(foo)"
console.log( Symbol('foo') + "bar" ); // TypeError: can't convert symbol to string
Symbols always convert to true
in a boolean context:
console.log( Boolean(Symbol()) ); // logs: true
if (Symbol()) { console.log("This will be logged.") }; // logs: "This will be logged."
Symbol property keys
Symbols are particularly used to create unique object property keys that won't collide with keys any other code might add to the object, and that are hidden from mechanisms other code will typically use to access the object's properties.
const symbolKey = Symbol(); // assigns a unique value to constant symbolKey
const someObj = {
[symbolKey]: "Some property value" // a property with a unique key
};
console.log( someObj[symbolKey] ); // logs: "Some property value"
BTW. The key name in the object literal in the example above is a
computed property name
(using []
).
In the example above the object's property cannot (accidentally) be overwritten by other code.
Also symbolKey
, holding the symbol value, cannot be overwritten because it is a constant.
If this constant's value is not a symbol, e.g. if it is a string, it can be overwritten:
const stringKey = "keyName";
const someObj = {
[stringKey]: "Some property value"
};
// note that:
console.log( someObj.keyName === someObj[stringKey] ); // logs: true
console.log( someObj[stringKey] ); // logs: "Some property value"
someObj.keyName = "Overwritten 😞";
console.log( someObj[stringKey] ); // logs: "Overwritten 😞"
Enumerability of symbol properties
Just like properties with string keys, properties with symbol keys are by default set to
writable, enumerable and configurable,
unless they are created with Object.defineProperty()
(which sets these attributes to false
by default).
And just like properties with string keys, attributes of properties with symbol keys can be changed by using Object.defineProperty()
.
const symbolKey = Symbol();
const someObj = {
[symbolKey]: "Some property value"
};
const descriptor = Object.getOwnPropertyDescriptor(someObj, symbolKey);
console.log(descriptor); // logs: { value: "Some property value", writable: true, enumerable: true, configurable: true }
Enumerable properties with string keys are visited in for...in
and Object.keys
enumerations.
However, symbol keyed properties are skipped, even if they are enumerable! As we can see in the tables of
MDN's article
"Enumerability and ownership of properties"
only Object.getOwnPropertySymbols
traverse exclusively symbol keyed properties.
A few methods, like Object.getOwnPropertyDescriptors
and Object.assign
, traverse both string and symbol keyed properties.
const someObj = {
prop1: 3,
prop2: "Hello",
[Symbol('symbol1')]: "symbol one",
[Symbol('symbol2')]: "symbol two"
};
const objectStrings = Object.keys(someObj);
console.log(objectStrings); // logs: [ "prop1", "prop2" ]
const objectSymbols = Object.getOwnPropertySymbols(someObj);
console.log(objectSymbols); // logs: [ Symbol("symbol1"), Symbol("symbol2") ]
objectSymbols.forEach( symbolKey => console.log( someObj[symbolKey]) );
// logs:
// "symbol one"
// "symbol two"
As shown in the example above, we can use symbol keys for properties that should be hidden from methods that code will typically use to traverse the object's properties.
The for...in
loop, for instance, skips symbol keyed properties. If we do want to traverse the symbol keyed properties, we can use
Object.getOwnPropertySymbols
.
When objects are copied or merged, one would probably want all properties to be involved.
Object.assign
, as well as spread syntax, can be used to clone or merge objects, including (own) symbol keyed properties (see previous chapter).
Global symbols
The Symbol
method Symbol.for(key)
creates a symbol that can be retrieved by
repeating the same statement with the same key
.
The Symbol.for(key)
method searches for existing symbols in a
"global symbol registry" (see next section) with the given key
and either returns the symbol if found
or creates a new symbol in the global symbol registry with this key
.
Only symbols created with Symbol.for()
are kept in the global symbol registry, and these symbols are called global symbols
or shared symbols.
console.log(Symbol.for('foo') === Symbol.for('foo')); // logs: true
console.log( String(Symbol.for('foo')) ); // logs: "Symbol(foo)"
BTW. Note that the key
is also used as the description of the symbol.
Note that also global symbols are unique values. Also the keys are unique. Global symbols cannot be deleted from the global symbol registry, nor can they be overwritten.
Global symbols can be used as unique values with a unique, programmer defined name (the key
).
Note that the value itself is named, not a possible variable the value is assigned to.
Unlike local symbols (Symbol()
), global symbols are available across files and across realms (via the global symbol registry, see next section).
Global symbols may be used as property names, to hide them from the common traversing methods.
However, using a global symbol as a property key makes it possible to overwrite the property!
const myObj = {
[Symbol.for('foo')]: "some property value"
};
console.log( myObj[Symbol.for('foo')] ); // logs: "some property value"
myObj[Symbol.for('foo')] = "Overwritten 😞";
console.log( myObj[Symbol.for('foo')] ); // logs: "Overwritten 😞"
Global symbol registry
The "global symbol registry" is a (theoretical) concept to describe a (fictitious) record of global symbols
that are available only through the Symbol.for()
and Symbol.keyFor()
methods.
The Symbol.keyFor()
method retrieves a global symbol key from the global symbol registry for the given global symbol.
const globalSym = Symbol.for('someKey'); // define a global symbol
console.log(Symbol.keyFor(globalSym)); // logs: "someKey"
The global symbol registry is not the same or not a part of the global object. In fact, the global symbol registry is global to all associated realms. Global symbols are symbols that are available across files and across realms, each of which has its own global scope.
Well-known symbols
The Symbol
constructor provides a
number of "built-in" properties
that all are symbols themselves and that are called well-known symbols.
These well-known symbols are used as property names (often method names) that JavaScript "recognize" and use internally
to identify "protocols" for certain operations that the programmer can customize via the property's value and in this way customize the object's behavior.
Well-known Symbols allow the customizations to be hidden from mechanisms other code will typically use to access the object's properties.
Well-known symbols are constant across realms, but are not in the global symbol registry.
console.log(Symbol.keyFor(Symbol.iterator)); // logs undefined
In the next chapter we will cover the well-known symbol Symbol.toPrimitive
.