TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Symbols in `as const` objects should be unique symbols

Open nstepien opened this issue 1 year ago • 5 comments

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 avatar May 02 '23 14:05 nstepien

Possibly related: https://github.com/microsoft/TypeScript/issues/53276

fatcerberus avatar May 02 '23 16:05 fatcerberus

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

nstepien avatar May 02 '23 16:05 nstepien

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

fatcerberus avatar May 02 '23 16:05 fatcerberus

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 avatar May 02 '23 17:05 nstepien

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

nstepien avatar May 03 '23 14:05 nstepien

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 avatar Nov 24 '23 20:11 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)

rotu avatar Nov 25 '23 00:11 rotu

+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'));

JacobLey avatar Feb 12 '24 19:02 JacobLey