For what symbols are in the runtime language — uniqueness, registry, well-known symbols — see Symbols in JavaScript.
TypeScript adds static types on top of JavaScript symbols. The value rules are unchanged; the compiler tracks which symbol keys exist on an object and which types their values have.
Symbol-branded metadata on objects
String property names collide across libraries. A symbol key typed on the object documents an internal slot without showing up in Object.keys at runtime:
const logHandler = Symbol("logHandler");
type WithLog<T> = T & { [logHandler]?: (obj: T) => string;};
function addLog<T extends object>( obj: T, func: (obj: T) => string,): asserts obj is WithLog<T> { (obj as WithLog<T>)[logHandler] = func;}
function doLog(message: string, obj: WithLog<{ src: string }>) { const objStr = obj[logHandler]?.(obj) ?? String(obj); console.log(`${message} ${objStr}`);}The consumer keeps using normal string keys on the same object. Only code that imports logHandler can read or write the logging function, which the type checker enforces when you export the symbol from one module.
Declaring symbol keys on interfaces
Bracket notation in a type lists a specific symbol (or a computed symbol type) as a member:
declare const id: unique symbol;
interface Entity { [id]: string; name: string;}
const row: Entity = { [id]: "uuid-1", name: "Ada",};Use unique symbol for module-level constants so each declaration stays distinct in the type system. Symbol() inside a function returns a wider symbol type unless you narrow it.
keyof, spread, and enumeration
keyof on a type with both string and symbol keys includes both kinds. Control which keys you copy:
type StringKeys<T> = Extract<keyof T, string>;Object.getOwnPropertySymbols is still the runtime API for symbol keys. TypeScript does not infer symbol keys from arbitrary object literals unless you annotate or satisfy an interface that declares them.
Well-known symbols in typings
Lib typings declare built-ins such as Symbol.iterator and Symbol.toStringTag. Implementing them on a class uses the same syntax as JavaScript; the compiler checks signatures against the official interface:
class Counter { private n = 0;
[Symbol.iterator](): Iterator<number> { return { next: () => this.n < 3 ? { value: this.n++, done: false } : { value: undefined, done: true }, }; }}Practical defaults
- Export the symbol constant alongside helpers that read the slot so consumers share one typed key.
- Prefer
unique symbolfor module-scoped keys; usesymbolwhen accepting any symbol from callers. - Keep JSON-facing data on string keys; use symbol keys for in-memory protocols and decorators that should not serialize.