ZetCode

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.

creating_symbols.ts
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.

symbol_properties.ts
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.

symbol_iteration.ts
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.

global_symbols.ts
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.

unique_symbols.ts
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.

well_known_symbols.ts
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.

it_sym.ts
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.

to_primitive.ts
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.

match_sym.ts
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.

split_sym.ts
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

Source

TypeScript Handbook: Symbols

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.