TypeScript Symbol Type
last modified October 15, 2026
A symbol is a primitive value that is guaranteed to be unique. It is
created with the Symbol function and is mostly used as a
property key: a property with a symbol key cannot clash with any other
property, not even one with the same name. JavaScript also defines a set of
well-known symbols, such as Symbol.iterator, which let objects
customize how built-in operations treat them. In TypeScript, symbols have the
type symbol, and constants can have the more precise
unique symbol type.
Creating Symbols
Every call to Symbol returns a new, unique value. An
optional description helps with debugging.
const symbol1 = Symbol();
const symbol2 = Symbol("id");
const symbol3 = Symbol("id");
console.log(symbol1, symbol2);
console.log(symbol2.description, typeof symbol2);
console.log(symbol2.toString());
console.log((symbol2 as symbol) === symbol3);
Two symbols with the same description are still different values,
so the comparison is false. The description is only a label; it
is available through the description property and in the output
of toString. The cast to symbol is needed because
TypeScript gives each constant its own unique symbol type and
would otherwise reject the comparison as always false.
$ npx tsx creating_symbols.ts Symbol() Symbol(id) id symbol Symbol(id) false
Using Symbols as Object Properties
A symbol is used as a property key with square brackets, both when defining the property and when accessing it.
const idSymbol = Symbol("id");
const user = {
name: "Alice",
id: "public-id",
[idSymbol]: 12345,
};
console.log(user.id);
console.log(user[idSymbol]);
const internal = Symbol("internal");
Object.assign(user, { [internal]: "cache data" });
console.log(Object.keys(user));
The object has both a normal id property and a property
keyed by idSymbol; they do not interfere with each other. This is
why libraries use symbols to attach their own data to objects that belong to
someone else: no other code can accidentally overwrite the property, because
no other code has the symbol.
$ npx tsx symbol_properties.ts public-id 12345 [ 'name', 'id' ]
Symbols and Iteration
Symbol-keyed properties are skipped by most ways of listing properties, but they are not private.
const idSymbol = Symbol("id");
const user = {
name: "Alice",
age: 30,
[idSymbol]: 12345,
};
for (const key in user) {
console.log(key);
}
console.log(Object.keys(user));
console.log(JSON.stringify(user));
console.log(Object.getOwnPropertySymbols(user));
console.log(Reflect.ownKeys(user));
The for...in loop, Object.keys, and
JSON.stringify ignore the symbol property. It is still reachable:
Object.getOwnPropertySymbols lists the symbol keys, and
Reflect.ownKeys lists all keys. Symbols hide properties from
accidental use, not from deliberate access; private data belongs in
#private class fields.
$ npx tsx symbol_iteration.ts
name
age
[ 'name', 'age' ]
{"name":"Alice","age":30}
[ Symbol(id) ]
[ 'name', 'age', Symbol(id) ]
Global Symbol Registry
Symbol.for returns a symbol from a global registry,
creating it on the first call. All code that asks for the same key gets the
same symbol.
const globalSymbol1: symbol = Symbol.for("app.userId");
const globalSymbol2: symbol = Symbol.for("app.userId");
const local: symbol = Symbol("app.userId");
console.log(globalSymbol1 === globalSymbol2);
console.log(globalSymbol1 === local);
console.log(Symbol.keyFor(globalSymbol1), Symbol.keyFor(local));
The two calls to Symbol.for return the identical symbol,
which makes registered symbols useful for sharing between modules, or even
between different realms such as iframes. A symbol created with
Symbol is not in the registry, so Symbol.keyFor
returns undefined for it. The variables are annotated as
symbol; without the annotations, each would have its own
unique symbol type and the compiler would reject the comparisons
as always false.
$ npx tsx global_symbols.ts true false app.userId undefined
Unique Symbols
The unique symbol type belongs to one specific symbol.
It allows symbols to be used as property keys in types.
const version: unique symbol = Symbol("version");
const author = Symbol("author");
interface VersionedDoc {
title: string;
[version]: number;
[author]: string;
}
const doc: VersionedDoc = {
title: "Report",
[version]: 3,
[author]: "Alice",
};
function bump(d: VersionedDoc): number {
d[version] += 1;
return d[version];
}
console.log(bump(doc), doc[author]);
A const declaration of a symbol gets a
unique symbol type automatically, so the annotation on
version is optional; author shows the inferred
form. With such constants as keys, the compiler knows the type of
doc[version] exactly, just like for properties with names. A
let variable has the general symbol type; used as a
key in an interface, it acts like an index signature for all symbols, so the
property types of different symbol keys are merged into a union and any symbol
is accepted. Symbol keys should therefore be declared with
const.
$ npx tsx unique_symbols.ts 4 Alice
Symbol.toStringTag
Well-known symbols are predefined symbols that built-in operations
look for. Symbol.toStringTag changes the result of the default
toString method.
class Temperature {
constructor(private celsius: number) {}
get [Symbol.toStringTag](): string {
return "Temperature";
}
}
const plain = {};
const tagged = { [Symbol.toStringTag]: "CustomObject" };
console.log(Object.prototype.toString.call(plain));
console.log(String(tagged));
console.log(Object.prototype.toString.call(new Temperature(21)));
Without the tag, objects describe themselves as
[object Object]. The tag can be a property of an object literal
or a getter in a class. Built-in types such as Map and
Promise use the same mechanism, which is why they print as
[object Map] and [object Promise].
$ npx tsx well_known_symbols.ts [object Object] [object CustomObject] [object Temperature]
Symbol.iterator
An object with a [Symbol.iterator] method is iterable:
it works with for...of, the spread operator, and
destructuring.
class Grades implements Iterable<string> {
private values = ["A", "B", "C", "D", "E", "FX"];
*[Symbol.iterator](): Generator<string> {
yield* this.values;
}
}
const grades = new Grades();
for (const grade of grades) {
console.log(grade);
}
console.log([...grades].join(" > "));
The generator method yields the grades one by one. Because the class implements the iteration protocol, it can be used with every construct that accepts iterables. The iterators tutorial covers the protocol in detail.
$ npx tsx it_sym.ts A B C D E FX A > B > C > D > E > FX
Symbol.toPrimitive
Symbol.toPrimitive controls how an object is converted
to a primitive value, for example in arithmetic or string contexts.
class Money {
constructor(private cents: number, private currency: string) {}
[Symbol.toPrimitive](hint: "number" | "string" | "default"): string | number {
if (hint === "number") {
return this.cents / 100;
}
return `${(this.cents / 100).toFixed(2)} ${this.currency}`;
}
}
const price = new Money(1999, "EUR");
console.log(`${price}`);
console.log(+price * 2);
console.log("Total: " + price);
The method receives a hint that tells which kind of value is
expected. A template literal asks for a string, the unary plus asks for a
number, and the + operator with a string asks for the default,
which this class formats as a string as well. Without the method, the object
would turn into [object Object] or NaN.
$ npx tsx to_primitive.ts 19.99 EUR 39.98 Total: 19.99 EUR
Symbol.match
Symbol.match lets an object act as the pattern in
String.prototype.match.
class CustomMatcher {
constructor(private word: string) {}
[Symbol.match](str: string): RegExpMatchArray | null {
const index = str.toLowerCase().indexOf(this.word.toLowerCase());
if (index === -1) {
return null;
}
const match = [str.slice(index, index + this.word.length)] as RegExpMatchArray;
match.index = index;
return match;
}
}
const matcher = new CustomMatcher("typescript");
const result = "I love TypeScript!".match(matcher);
console.log(result?.[0], result?.index);
console.log("I love Go!".match(matcher));
The matcher searches case-insensitively and returns an array with the
matched text and its index, like a regular expression match, or
null when nothing is found. TypeScript requires this return
type for objects passed to match.
$ npx tsx match_sym.ts TypeScript 7 null
Symbol.split
Symbol.split lets an object act as the separator in
String.prototype.split.
class CustomSplitter {
constructor(private separators: string) {}
[Symbol.split](str: string): string[] {
const parts: string[] = [];
let current = "";
for (const char of str) {
if (this.separators.includes(char)) {
if (current) {
parts.push(current.trim());
}
current = "";
} else {
current += char;
}
}
if (current) {
parts.push(current.trim());
}
return parts;
}
}
const result = "1, 2;3;;4 ,5".split(new CustomSplitter(",;"));
console.log(result);
The splitter treats both commas and semicolons as separators, skips
empty parts, and trims whitespace. The split method calls the
object's [Symbol.split] method instead of using its built-in
algorithm. Other well-known symbols, such as Symbol.replace and
Symbol.search, work the same way for the corresponding string
methods.
$ npx tsx split_sym.ts [ '1', '2', '3', '4', '5' ]
Best Practices
- Use Symbols for Collision-Free Keys: Attach metadata to objects you do not own with symbol keys.
- Do Not Rely on Them for Privacy: Symbol properties are hidden from casual iteration but accessible; use
#privatefields for real privacy. - Register Shared Symbols: Use
Symbol.forwith a namespaced key such as"app.userId"when several modules need the same symbol. - Declare Symbol Keys as const: Only
unique symbolconstants give symbol-keyed properties their own precise types. - Implement Well-Known Symbols: Make classes iterable and printable with
Symbol.iterator,Symbol.toStringTag, andSymbol.toPrimitive.
Source
This tutorial covered symbols in TypeScript. They provide property keys that cannot clash with other keys, and well-known symbols let objects plug into built-in language features.
Author
List all TypeScript tutorials.