Skip to content

Symbols in as const objects should be unique symbolsΒ #54100

Open

Description

Bug Report

πŸ”Ž Search Terms

symbol object "as const"

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about symbol

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

const A = Symbol();
const B = Symbol();
const MyEnumA = { A, B } as const;
//    ^?
type MyEnumA = typeof MyEnumA[keyof typeof MyEnumA];
//    ^?
// good

function testA(val: MyEnumA) {}
testA(MyEnumA.A);
testA(MyEnumA.B);
testA(Symbol()); // good type error

const MyEnumB = { C: Symbol(), D: Symbol() } as const;
//    ^?
// should not use generic "symbol" type
type MyEnumB = typeof MyEnumB[keyof typeof MyEnumB];
//    ^?
// should not be symbol

function testB(val: MyEnumB) {}
testB(MyEnumB.C);
testB(MyEnumB.D);
testB(Symbol()); // should be a type error

πŸ™ Actual behavior

Symbols in as const-ed objects should be unique symbols, instead they're of type symbol.

πŸ™‚ Expected behavior

When I write

const obj = { a: Symbol() } as const;

I want obj.a to be a unique symbol.

What I'm trying to do is replace TS enums with symbol-based object "enums" in some scenarios, as it gives me greater typecheck-time and runtime guarantees.

I can do

const A = Symbol();
const MyEnum = { A } as const;
type MyEnum = typeof MyEnum[keyof typeof MyEnum];

and that works great, but I end up with many const ... = Symbol() which pollute the scope and can be misused, when I'd rather do

const MyEnum = { A: Symbol() } as const;
type MyEnum = typeof MyEnum[keyof typeof MyEnum];

Activity

  1. changed the title [-]`as const` objects should have unique symbols[/-] [+]Symbols in `as const` objects should be unique symbols[/+] on May 2, 2023
  2. fatcerberus commented on May 2, 2023

    @fatcerberus

    Possibly related: #53276

  3. nstepien commented on May 2, 2023

    @nstepien
    Author

    Bruce Pascoe (@fatcerberus) I'm not convinced this issue is related.

  4. fatcerberus commented on May 2, 2023

    @fatcerberus

    Nicolas Stepien (@nstepien) Note I said "related", not "duplicate".

    That said, I think the limitation here is that unique symbols explicitly have type typeof x, which implies there must be an x in scope that can be used with typeof. Given an anonymous object literal containing Symbol() calls, there's nothing for typeof to refer back to.

  5. nstepien commented on May 2, 2023

    @nstepien
    Author

    Given

    const obj = { x: Symbol() } as const

    , would it be possible for its type to be typeof obj.x?

    Although that wouldn't work in a case like

    fn({ [dynamicKey]: Symbol() } as const)

    as there is no specific name to refer back to.

    Maybe unique symbol is good enough.

  6. nstepien commented on May 3, 2023

    @nstepien
    Author

    I wonder if TS could use the description when available, i.e. Symbol('desc')'s type could be typeof Symbol('desc')
    or unique symbol 'desc'.

  7. rotu commented on Nov 24, 2023

    @rotu

    could be typeof Symbol('desc') or unique symbol 'desc'.

    That's expressible as declare const mySymbol: symbol & {readonly description:'desc'}. Unfortunately, you have to give up uniqueness. (unique symbol) & {readonly description:'desc'} silently ignores the uniqueness. And {readonly description:'desc'} & (unique symbol) errors "'unique symbol' types are not allowed here."

    Trying to fix this with an explicit annotation fails:

    // ERROR: Type 'symbol' is not assignable to type 'unique symbol'.
    const myEnum: {readonly a:unique symbol} = { a: Symbol() };

    The best I achieve is with a cast:

    const myEnum = { a: Symbol() } as { readonly a: unique symbol }

    But this doesn't provide the type safety you seek because unique symbols decay too easily:

    const myEnum = { a: Symbol() } as { readonly a: unique symbol }
    const anotherEnum = { a: Symbol() } as { readonly a: unique symbol }
    
    let b = myEnum.a
    b = anotherEnum.a // OOPS! NO ERROR!
  8. rotu commented on Nov 25, 2023

    @rotu

    Wrote up some of the related issue in #56535 (namely that the type system disregards the description passed in to the Symbol constructor, even if it's a string literal)

  9. JacobLey commented on Feb 12, 2024

    @JacobLey

    +1 to this feature request.

    Running into a similar issue where I would like the type generic to prefer a unique symbol.

    e.g.

    const doGenericThing = <T extends symbol>(val: T): { val: T } => {
        return { val };
    };
    

    Doesn't use unique symbol by default:

    // { val: symbol }
    const notUnique = doGenericThing(Symbol('abc'));
    

    Can assign to a const first, which works but requires a technically unnecessary variable declaration

    const uniqueSym = Symbol('abc');
    // { val: typeof uniqueSym }
    const unique = doGenericThing(uniqueSym);
    

    Appending as const seems like a very reasonable approach to have it use a unique symbol.

    // Not yet legal
    const standaloneUnique = doGenericThing(Symbol('abc') as const);
    

    Possibly off topic, but I wouldn't expect any of this unique functionality to apply to Symbol.for, unless symbols were to start tracking their description (see issue linked above).

    However it appears that is not the case:

    const sym = Symbol.for('abc');
    const restrictToSym = (val: typeof sym) => {};
    
    // _should_ work, but treated as a separate unique symbol
    restrictToSym(Symbol.for('abc'));
    
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

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions