Repository navigation
Symbol type is incorrectly generalized when used as a property value of an object literal #36876
Description
Activity
- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Feb 20, 2020 RyanCavanaugh commented
on Feb 20, 2020 MemberMore actionsRon Buckton (@rbuckton) I believe this is the intended behavior, but can you provide context?
Same is true of other literal types:
const uniqueSymbol = "tag" const foo = { prop: uniqueSymbol, } type Foo = typeof foo
propis a mutable object member. Assigning a singleton type to a mutable member is usually not useful, so we widen at mutable positions. Do note, there is no way to declare an immutable object property (only class fields).Reacted by Paul MellerTaking into consideration all primitive types this seems to be consistent. I just intuitively expected something else as a programmer since
symbols are unique values.My specific (simiplified) use case to give you more context (React application):
// Application state business logic const loading = Symbol() type Resource = { id: number; name: string } // `loading` is used to encode loading state, `undefined` for not found const getResource: () => Observable<typeof loading | undefined | Resource> = ... const useApplicationState = () => { const resource: typeof loading | undefined | Resource = useObservable(() => getResource(), []) return { resource, // type of symbol is widened here } } // UI component const { resource } = useApplicationState() if (resource === loading) { // render spinner } else if (resource) // render resource details // this won't work as `typeof resource` is `Resource | symbol`, expected was just `Resource` } else { // render not found }
I ended up with type casting as the simpliest solution:
const useApplicationState = () => { const resource: typeof loading | undefined | Resource = useObservable(() => getResource(), []) return { resource: resource as typeof resource, } }
Wesley Wigham (@weswigham) Please note that there is quite simple way to enforce string literal type for string primitives, whereas it's not possible for symbols (because their type isn't known before initialization):
type UniqueString = 'UNIQUE_STRING' const uniqueString: UniqueString = 'UNIQUE_STRING' const foo = { prop: uniqueString, } type Foo = typeof foo // { prop: 'UNIQUE_STRING' } const uniqueSymbol: unique symbol = Symbol() type UniqueSymbol = typeof uniqueSymbol const bar = { prop: uniqueSymbol, } type Bar = typeof bar // { prop: symbol }
It seems that main issue that I've described works consistently for primitive types but at the same time TS as a language lacks other mechanisms for type
symbolthat allow to avoid generalization of variable that is a specific symbol. Please take a look at following techniques:const literalString0 = 'literalString1' const literalNumber0 = 1 const literalBoolean0 = true // literal types are widened const obj0 = { literalString0, literalNumber0, literalBoolean0, } // literal types are preserved // technique #1 using `as const` const literalString1 = 'literalString1' as const const literalNumber1 = 1 as const const literalBoolean1 = true as const const obj1 = { literalString1, literalNumber1, literalBoolean1, } // technique #2 using explicit type declaration const literalString2: 'literalString1' = 'literalString1' const literalNumber2: 1 = 1 const literalBoolean2: true = true const obj2 = { literalString2, literalNumber2, literalBoolean2, }
Both
as constcasting and explicit type declaration cannot be used onsymbols.I am seeing this issue in a project using JSDoc syntax.
I found I was able to work around it by declaring consts and then assembling them into a frozen object:
//@ts-check // option 1: assemble from consts, works correctly const FOO = Symbol.for('FOO'); const BAR = Symbol.for('BAR'); const MySymbols = Object.freeze({ FOO: FOO, BAR: BAR, }); // // option 2: declare inline, results in non-specialized `symbol` type for both FOO and BAR // const MySymbols = Object.freeze({ // FOO: Symbol.for('FOO'), // BAR: Symbol.for('BAR'), // }); const ZAP = Symbol.for('ZAP'); /** @typedef {typeof MySymbols[keyof MySymbols]} ValidSymbol */ /** @type {ValidSymbol} */ const shouldFail = ZAP; // TypeScript only raises an issue with this assignment for option 1 /** @type {ValidSymbol} */ const shouldWork = MySymbols.FOO;
However, this pollutes the surrounding namespace with extra variables...
Perhaps this workaround (
Object.freeze'ing a literal object assembled from constants) also applies to TS syntax.
TypeScript Version: 3.7.5
Search Terms: symbol infer type
Expected behavior:
Symbol
const uniqueSymbol = Symbol()when used as property value in an object literal is inferred as unique typetypeof uniqueSymbol.Actual behavior:
Symbol
const uniqueSymbol = Symbol()when used as property value in an object literal is inferred as general typesymbol.Related Issues:
Code
Output
Compiler Options
{ "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictPropertyInitialization": true, "strictBindCallApply": true, "noImplicitThis": true, "noImplicitReturns": true, "useDefineForClassFields": false, "alwaysStrict": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "downlevelIteration": false, "noEmitHelpers": false, "noLib": false, "noStrictGenericChecks": false, "noUnusedLocals": false, "noUnusedParameters": false, "esModuleInterop": true, "preserveConstEnums": false, "removeComments": false, "skipLibCheck": false, "checkJs": false, "allowJs": false, "declaration": true, "experimentalDecorators": false, "emitDecoratorMetadata": false, "target": "ES2017", "module": "ESNext" } }Playground Link: Provided