TypeScript
TypeScript copied to clipboard
Symbols in `as const` objects should be unique symbols
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 symbol
s, 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];
Possibly related: https://github.com/microsoft/TypeScript/issues/53276
@fatcerberus I'm not convinced this issue is related.
@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.
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.
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'
.
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 unique
ness. 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!
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)
+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'));