TS Symbol identity is ineffective
TypeScript's typing of symbol identity clashes with actual JavaScript semantics, making standard, idiomatic use of JS symbols difficult or impossible.
Search terms
Symbol.for, global symbol registry, unique symbol, well-known symbol, computed symbol property, declaration emit
Version and reproduction
The supplied Playground link selects TypeScript 6.0.3. I have not tested it against newer TypeScript versions.
Playground reproduction (TypeScript 6.0.3)
JavaScript behavior
Symbol.for("twin") returns the same symbol whenever the registry key is "twin"; Symbol("unique") creates a fresh symbol on each call. Well-known symbols such as Symbol.match are stable values. The types of references to these values should reflect those identities where the key or symbol is known.
Code
The code below is copied from the Playground link above.
// The code I want to use:
// /**
const single = Symbol.for('single');
const twin1 = Symbol.for('twin');
const twin2 = Symbol.for('twin');
const uniq = Symbol('unique');
const wellKnown = Symbol.match;
const wellKnown2 = Symbol.toStringTag;
// */
/**
// This is the .d.ts code generated from above (without the shim).
declare const single: unique symbol; // invalid - needs type like RegisteredSymbol<'single'>
declare const twin1: unique symbol; // invalid - needs type like RegisteredSymbol<'twin'>
declare const twin2: unique symbol; // invalid - needs type like RegisteredSymbol<'twin'>
declare const uniq: unique symbol; // valid
declare const wellKnown: symbol; // invalid - needs to be `typeof Symbol.match`
declare const wellKnown2: symbol; // invalid - needs to be `typeof Symbol.toStringTag`
*/
const record1 = {
[single]: 'single',
[twin2]: 'twin',
[uniq]: 'uniq',
} as const;
let singleVal: string = record1[single] satisfies 'single';
// @ ts-expect-error why are multiple Symbol.for('twin') lookups not identical?
// Property '__@twin1@10' does not exist on type \
// '{ readonly [single]: "single"; readonly [twin2]: "twin"; readonly [uniq]: "uniq"; }'. \
// Did you mean '__@twin2@11'? (2551)
let twin1Val: string = record1[twin1] satisfies 'twin';
let twin2Val: string = record1[twin2] satisfies 'twin';
let uniqVal: string = record1[uniq] satisfies 'uniq';
const record2 = {
...record1,
[uniq]: 'uniqNew',
[wellKnown]: 'wellKnown',
} as const;
singleVal = record2[single] satisfies 'single';
// @ ts-expect-error Symbol.for('twin') lookups still aren't identical.
// Property '__@twin1@10' does not exist on type \
// '{ readonly [uniq]: "uniqNew"; readonly [single]: "single"; readonly [twin2]: "twin"; }' \
// Did you mean '__@twin2@11'? (2551)
twin1Val = record2[twin1] satisfies 'twin';
twin2Val = record2[twin2] satisfies 'twin';
uniqVal = record2[uniq] satisfies 'uniqNew';
// @ ts-expect-error Extending a const spread with a well-known symbol makes strange.
// Element implicitly has an 'any' type because expression of type 'symbol' can't be used \
// to index type '{ readonly [uniq]: "uniqNew"; readonly [single]: "single"; \
// readonly [twin2]: "twin"; }'. (7053)
let wellKnownVal: string = record2[wellKnown] satisfies 'wellKnown';
const record3 = {
[single]: 'single',
[twin2]: 'twin',
[uniq]: 'uniq',
[wellKnown]: 'wellKnown',
} as const;
singleVal = record3[single] satisfies 'single';
// @ ts-expect-error Presence of well-known symbol causes spurious union values.
// Type '"single" | "twin" | "uniq" | "wellKnown"' does not satisfy the expected type '"twin"'.
// Type '"single"' is not assignable to type '"twin"'. (1360)
twin1Val = record3[twin1] satisfies 'twin';
twin2Val = record3[twin2] satisfies 'twin';
uniqVal = record3[uniq] satisfies 'uniq';
// @ ts-expect-error Presence of well-known symbol causes spurious union values.
// Type '"single" | "twin" | "uniq" | "wellKnown"' does not satisfy the expected type '"wellKnown"'.
// Type '"single"' is not assignable to type '"wellKnown"'. (1360)
wellKnownVal = record3[wellKnown] satisfies 'wellKnown';
Actual behavior
- Separate
Symbol.for("twin") calls are treated as separate unique symbol identities, so a property written using one call's result cannot reliably be read using the other's, even though both values are the same at runtime.
- A
const alias of a well-known symbol can widen to symbol. This loses the exact computed property key through object literals and spreads, producing erroneous indexed-access errors or unions of unrelated property values.
- Declaration output can describe registered symbols as unrelated
unique symbol values and well-known symbol aliases as plain symbol, losing useful identity information for consumers.
Expected behavior
- Equal literal registry keys identify the same registered symbol type; different keys remain distinct. Unknown or union keys must stay conservative rather than acquire one fabricated unique identity.
- Const aliases of known symbols retain their identity, and symbol-keyed property access resolves to the corresponding property's value type, including after a spread.
- Emitted declarations preserve the symbol identity that consumers need to use those keys.
Related work
TS Symbol identity is ineffective
TypeScript's typing of symbol identity clashes with actual JavaScript semantics, making standard, idiomatic use of JS symbols difficult or impossible.
Search terms
Symbol.for, global symbol registry,unique symbol, well-known symbol, computed symbol property, declaration emitVersion and reproduction
The supplied Playground link selects TypeScript 6.0.3. I have not tested it against newer TypeScript versions.
Playground reproduction (TypeScript 6.0.3)
JavaScript behavior
Symbol.for("twin")returns the same symbol whenever the registry key is"twin";Symbol("unique")creates a fresh symbol on each call. Well-known symbols such asSymbol.matchare stable values. The types of references to these values should reflect those identities where the key or symbol is known.Code
The code below is copied from the Playground link above.
Actual behavior
Symbol.for("twin")calls are treated as separateunique symbolidentities, so a property written using one call's result cannot reliably be read using the other's, even though both values are the same at runtime.constalias of a well-known symbol can widen tosymbol. This loses the exact computed property key through object literals and spreads, producing erroneous indexed-access errors or unions of unrelated property values.unique symbolvalues and well-known symbol aliases as plainsymbol, losing useful identity information for consumers.Expected behavior
Related work
Symbol.forreports that TypeScript treats two calls with the same registry key as disjointunique symboltypes. Thetwin1/twin2property accesses above show another consequence of that mismatch with JavaScript identity.constalias of a known symbol to retain the original symbol type. Losing that identity affects thewellKnowncomputed property and its subsequent accesses in this repro.unique symbollost on assignment toconstdespite type assertion describes a related widening case:const a = u as typeof uis inferred assymbolrather thantypeof u. It illustrates why explicit assertions should not discard a symbol's known identity.RegisteredSymbol<Key>as the type of the memoizedunique symbolpermanently associated withKeyin the global symbol registry. It also preserves narrow symbol types in const-like declarations and updates symbol-keyed property handling; its tests cover this repro and ensure unresolved keys do not acquire a fabricated single identity.