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 TypeErrorSymbol 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