symbol ist der unscheinbarste der sieben Primitive-Typen — und gleichzeitig derjenige, dem du am häufigsten begegnest, ohne ihn zu bemerken. Jedes for…of über ein Array, jede async function*, jedes using-Statement aus TS 5.2 stützt sich im Hintergrund auf einen Symbol-Property-Key. Direkt selbst Symbols zu erzeugen ist dagegen Nischen-Arbeit, taucht aber zuverlässig auf, sobald du eigene Iterables baust, fremde Objekte um Metadaten ergänzen willst oder das Disposable-Protokoll für dein eigenes Resource-Handling implementierst. Dieser Artikel zeigt, warum Symbols garantiert eindeutig sind, was den TypeScript-eigenen unique symbol-Sub-Typ ausmacht und wo sich Symbols, well-known Symbols und private Klassenfelder ähneln — und wo nicht.

Was Symbols sind

Symbol ist seit ES2015 ein eigenständiger Primitive-Typ neben string, number, boolean, bigint, null und undefined. Ein Symbol-Wert entsteht durch den Funktionsaufruf Symbol() — niemals mit new. Jeder Aufruf liefert einen frischen, eindeutigen Wert, selbst wenn du dieselbe Beschreibung mitgibst.

ts symbol-basics.ts
const a = Symbol("id");
const b = Symbol("id");

console.log(typeof a);   // "symbol"
console.log(a === b);    // false — jedes Symbol ist einzigartig
console.log(a.description); // "id" — nur Debug-Hilfe
Output
symbol
false
id

Die optionale Beschreibung ("id") ist kein Identitätsmerkmal, sondern reine Debug-Information für toString() und Devtools. Identität läuft strikt über Referenz — a === a ist true, a === b ist false, immer.

new Symbol() wirft zur Laufzeit einen TypeError. TypeScript fängt diesen Versuch bereits beim Type-Check ab, weil Symbol keinen Construct-Signature-Eintrag hat.

symbol als Typ

Der Typ symbol ist die Mengensicht über alle Symbol-Werte — analog zu string für alle Strings oder number für alle Zahlen. Auf Typ-Ebene ist symbol damit kein eindeutiger Typ. Zwei Werte vom Typ symbol können denselben oder unterschiedlichen Laufzeit-Wert haben.

ts symbol-typ.ts
function speichere(key: symbol, value: unknown) {
    // key ist irgendein Symbol — TS weiß nicht, welches
}

const userId = Symbol("userId");
speichere(userId, 42); // ok

Für Werkzeug-Funktionen, die mit beliebigen Symbols arbeiten, ist das genau der richtige Typ. Sobald du aber ein konkretes Symbol als Property-Key in einem Interface verankern willst, reicht symbol nicht mehr — dafür gibt es unique symbol.

unique symbol

unique symbol ist ein TypeScript-spezifischer Sub-Typ von symbol. Er bezeichnet genau einen einzigen Symbol-Wert — den, der bei einer bestimmten const-Deklaration entstanden ist. Damit lässt sich ein Symbol als Property-Key in Type-Definitionen verwenden, weil der Compiler die Identität verlässlich nachverfolgen kann.

ts unique-symbol.ts
const KEY: unique symbol = Symbol("key");
// let würde NICHT funktionieren — let kann reassignt werden,
// also kann die Identität nicht garantiert werden.

interface Box {
    [KEY]: number;
}

const b: Box = { [KEY]: 42 };
b[KEY]; // number

// Vergleich auf Typ-Ebene
const OTHER: unique symbol = Symbol("key");
if (KEY === OTHER) {
    // Fehler: This comparison appears to be unintentional
    //         because the types have no overlap.
}

Zwei Punkte sind dabei wichtig:

  • unique symbol ist nur auf const-Deklarationen und static readonly-Klassenfeldern erlaubt — alles, was reassigniert werden könnte, verliert die garantierte Identität.
  • Auf eine bestimmte unique symbol-Identität greifst du in fremden Type-Positionen über typeof KEY zu, nicht über unique symbol selbst (das wäre wieder ein neuer eindeutiger Typ).
ts typeof-unique-symbol.ts
const KEY: unique symbol = Symbol();

function read(obj: { [KEY]: number }): number {
    return obj[KEY];
}

// Alternative Schreibweise mit typeof in einer anderen Datei:
type WithKey = { [k: typeof KEY]: number };

Symbol als Property-Key

Der Hauptanwendungsfall für selbst-erzeugte Symbols ist das Setzen von kollisionsfreien Property-Keys. Ein String-Key wie "id" kann mit existierenden oder zukünftigen Properties kollidieren — ein Symbol-Key niemals, weil der Wert nur dort existiert, wo du ihn exportierst.

ts symbol-key.ts
// bibliothek.ts
export const META: unique symbol = Symbol("lib.meta");

// anwendung.ts
import { META } from "./bibliothek";

interface User {
    name: string;
    [META]?: { internalId: number };
}

const u: User = { name: "Ada", [META]: { internalId: 7 } };

console.log(u.name);           // "Ada"
console.log(u[META]?.internalId); // 7
console.log(Object.keys(u));    // ["name"] — Symbol-Key unsichtbar
Output
Ada
7
[ 'name' ]

Symbol-Keys werden bei Object.keys, for…in und JSON.stringify stillschweigend übergangen. Das ist der zentrale Trick für Bibliotheks-Erweiterungen: Du hängst Metadaten an Fremd-Objekte, ohne deren öffentliche Form zu verändern.

Wer Symbol-Keys gezielt auslesen will, nutzt Object.getOwnPropertySymbols(obj) oder Reflect.ownKeys(obj) — letzteres liefert String- und Symbol-Keys gemeinsam.

Well-known Symbols

JavaScript selbst nutzt eine Handvoll vordefinierter Symbols, um Protokolle ans Sprachkern-Verhalten anzukoppeln. Wenn du diese Symbols als Methoden implementierst, hängt sich dein Objekt in Built-in-Operatoren ein.

SymbolWofür
Symbol.iteratorMacht ein Objekt für for…of iterierbar.
Symbol.asyncIteratorAsync-Iteration via for await…of.
Symbol.toPrimitiveSteuert Konvertierung zu string/number/default.
Symbol.hasInstanceAnpassbares Verhalten für instanceof.
Symbol.toStringTagLiefert den Tag in Object.prototype.toString.call(x).
Symbol.disposeSync-Cleanup für using (TS 5.2 / ES2024-Stage-3).
Symbol.asyncDisposeAsync-Cleanup für await using.

In TypeScripts Standard-Lib sind diese Symbols als unique symbol-Konstanten unter SymbolConstructor deklariert, weshalb du sie direkt als Property-Keys in Interfaces nutzen kannst.

Symbol.iterator implementieren

Das wahrscheinlich häufigste Anwendungs-Szenario in echtem Code: ein eigenes Iterable bauen. Die formelle Bedingung lautet — ein Objekt muss eine Methode unter Symbol.iterator haben, die einen Iterator zurückgibt (Objekt mit next(), das { value, done } liefert).

ts iterable-range.ts
class Range implements Iterable<number> {
    constructor(private start: number, private end: number) {}

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

for (const n of new Range(1, 4)) {
    console.log(n);
}
Output
1
2
3

Für for await…of ersetzt du Symbol.iterator durch Symbol.asyncIterator und gibst einen AsyncIterator zurück, dessen next() ein Promise liefert.

Symbol-Registry mit Symbol.for

Symbol() erzeugt ein Symbol mit Modul-lokaler Identität — zwei Aufrufe sind nie gleich, auch nicht modulübergreifend. Wenn du quer durch Module oder sogar über iframe-Grenzen hinweg denselben Symbol-Wert teilen willst, nutzt du die globale Registry über Symbol.for(key).

ts symbol-for.ts
const a = Symbol.for("app.token");
const b = Symbol.for("app.token");

console.log(a === b); // true — gleiche Registry-Eintragung
console.log(Symbol.keyFor(a)); // "app.token"

const local = Symbol("app.token");
console.log(a === local); // false — Symbol() ist NICHT in der Registry
Output
true
app.token
false

Faustregel:

  • Symbol() — Default. Lokal, garbage-collectable, in WeakMap/WeakSet erlaubt.
  • Symbol.for(key) — nur wenn du explizit über Modul-/Realm-Grenzen synchronisieren musst. Registry-Einträge werden nie wieder freigegeben.

Symbol vs. private Klassenfelder (#)

Sowohl Symbol-Keys als auch ECMAScript-private Felder (#name) eignen sich für Encapsulation — aber sie lösen unterschiedliche Probleme.

AspektSymbol-Key#-Field
Zugriff von außenMöglich, wenn das Symbol exportiert wird.Niemals — syntaktisch nur in der Klasse selbst.
Über Klasse hinausFrei verteilbar als Konstante.An Klassen-Body gebunden.
ReflexionObject.getOwnPropertySymbols listet auf.Nicht reflektierbar, völlig versteckt.
JSON.stringifyWird übergangen.Erscheint nicht — Felder sind nicht enumerable.
VererbungSubklassen können Key importieren und ergänzen.Privat, in Subklassen nicht zugreifbar.

Praktischer Daumen: Wenn du echte Privatheit willst und der Wert die Klasse nie verlässt, nimm #. Wenn du kollisionsfreie Identität brauchst, die du selektiv freigibst (z. B. ein Protokoll wie Symbol.iterator), nimm Symbols.

Besonderheiten

Symbol ohne new aufrufen.

new Symbol() wirft zur Laufzeit einen TypeError — Symbol ist keine Constructor-Funktion, sondern eine Factory. TypeScript meldet das schon beim Type-Check, weil SymbolConstructor keine Construct-Signature hat. Immer als reine Funktion aufrufen: const s = Symbol("id");.

const vs. let entscheidet über unique symbol.

Nur eine const-Deklaration kann den Sub-Typ unique symbol tragen. let x: unique symbol ist ein Compile-Fehler, weil let reassignt werden könnte und damit die Identität verloren ginge. Sobald du das Symbol als Property-Key in einem Interface nutzen willst, ist const Pflicht.

Symbol.iterator macht ein Objekt for…of-fähig.

Ein Objekt wird zum Iterable, sobald es eine Methode unter Symbol.iterator hat, die einen Iterator zurückgibt. TypeScript stellt dafür die Interfaces Iterable<T> und Iterator<T> bereit — implementieren und fertig.

Symbol.asyncIterator für for await of.

Analog zum synchronen Iterator, aber next() liefert ein Promise<IteratorResult<T>>. async function* erzeugt automatisch ein Objekt mit korrekt gesetztem [Symbol.asyncIterator], also reicht in den meisten Fällen ein Async-Generator.

Symbol.dispose und Symbol.asyncDispose ab TS 5.2.

Mit dem neuen using- und await using-Statement bindet TypeScript Cleanup an Symbol-Keys: ein Objekt mit Symbol.dispose wird beim Scope-Verlassen automatisch aufgeräumt. Damit lassen sich Datei-Handles, DB-Verbindungen und Locks deterministisch schließen, ohne try…finally.

Symbol-Keys werden von JSON.stringify ignoriert.

JSON.stringify({ [Symbol("x")]: 1, a: 2 }) liefert {"a":2} — Symbol-Properties fehlen kommentarlos. Praktisch für interne Metadaten, gefährlich, wenn du dich auf Vollständigkeit verlässt. Wer Symbols serialisieren muss, schreibt eine eigene toJSON()-Methode oder nutzt Reflect.ownKeys.

Object.keys versteckt Symbol-Keys.

Weder Object.keys noch for…in oder Object.entries liefern Symbol-Properties. Sichtbar werden sie nur über Object.getOwnPropertySymbols(obj) bzw. Reflect.ownKeys(obj) — letzteres listet String- und Symbol-Keys zusammen.

Symbol.for ist global, Symbol() ist lokal.

Symbol("k") erzeugt jedes Mal ein frisches Symbol — perfekt für modul-private Identitäten. Symbol.for("k") dagegen schlägt zuerst in der globalen Registry nach und gibt bei Wiederverwendung dasselbe Symbol zurück, auch über Realm-Grenzen. Registry-Einträge werden nicht garbage-collected.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Primitive Typen

Zur Übersicht