Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Symbols in as const objects should be unique symbols #54100

Open
nstepien opened this issue May 2, 2023 · 8 comments
Open

Symbols in as const objects should be unique symbols #54100

nstepien opened this issue May 2, 2023 · 8 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@nstepien
Copy link

nstepien commented May 2, 2023

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];
@nstepien nstepien changed the title as const objects should have unique symbols Symbols in as const objects should be unique symbols May 2, 2023
@fatcerberus
Copy link

Possibly related: #53276

@nstepien
Copy link
Author

nstepien commented May 2, 2023

@fatcerberus I'm not convinced this issue is related.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels May 2, 2023
@fatcerberus
Copy link

fatcerberus commented May 2, 2023

@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.

@nstepien
Copy link
Author

nstepien commented May 2, 2023

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.

@nstepien
Copy link
Author

nstepien commented May 3, 2023

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'.

@rotu
Copy link

rotu commented Nov 24, 2023

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!

@rotu
Copy link

rotu commented Nov 25, 2023

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)

@JacobLey
Copy link

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
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants