Skip to content

TS Symbol typing clashes with standard, idiomatic JS #64585

Description

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions