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.
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-Hilfesymbol
false
idDie 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.
function speichere(key: symbol, value: unknown) {
// key ist irgendein Symbol — TS weiß nicht, welches
}
const userId = Symbol("userId");
speichere(userId, 42); // okFü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.
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 symbolist nur aufconst-Deklarationen undstatic 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 übertypeof KEYzu, nicht überunique symbolselbst (das wäre wieder ein neuer eindeutiger Typ).
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.
// 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 unsichtbarAda
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.
| Symbol | Wofür |
|---|---|
Symbol.iterator | Macht ein Objekt für for…of iterierbar. |
Symbol.asyncIterator | Async-Iteration via for await…of. |
Symbol.toPrimitive | Steuert Konvertierung zu string/number/default. |
Symbol.hasInstance | Anpassbares Verhalten für instanceof. |
Symbol.toStringTag | Liefert den Tag in Object.prototype.toString.call(x). |
Symbol.dispose | Sync-Cleanup für using (TS 5.2 / ES2024-Stage-3). |
Symbol.asyncDispose | Async-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).
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);
}1
2
3Fü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).
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 Registrytrue
app.token
falseFaustregel:
Symbol()— Default. Lokal, garbage-collectable, inWeakMap/WeakSeterlaubt.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.
| Aspekt | Symbol-Key | #-Field |
|---|---|---|
| Zugriff von außen | Möglich, wenn das Symbol exportiert wird. | Niemals — syntaktisch nur in der Klasse selbst. |
| Über Klasse hinaus | Frei verteilbar als Konstante. | An Klassen-Body gebunden. |
| Reflexion | Object.getOwnPropertySymbols listet auf. | Nicht reflektierbar, völlig versteckt. |
| JSON.stringify | Wird übergangen. | Erscheint nicht — Felder sind nicht enumerable. |
| Vererbung | Subklassen 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.