Skip to content

Symbol type is incorrectly generalized when used as a property value of an object literal #36876

Description

@pmeller

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 type typeof uniqueSymbol.

Actual behavior:

Symbol const uniqueSymbol = Symbol() when used as property value in an object literal is inferred as general type symbol.

Related Issues:

Code

const uniqueSymbol = Symbol()

// Example of incorrect inference (unique symbol is generalized to type `symbol`)

const foo = {
  prop: uniqueSymbol,
}

type Foo = typeof foo  // { prop: symbol }

// Workarounds for incorrect behavior

const bar = {
  prop: uniqueSymbol as typeof uniqueSymbol,
}

type Bar = typeof bar // { prop: typeof uniqueSymbol }

const genericFactory = <T>(value: T) => ({ prop: value })

const baz = genericFactory(uniqueSymbol)

type Baz = typeof baz // { prop: typeof uniqueSymbol }
Output
"use strict";
const uniqueSymbol = Symbol();
// Example of incorrect inference (unique symbol is generalized to type `symbol`)
const foo = {
    prop: uniqueSymbol,
};
// Workarounds for incorrect behavior
const bar = {
    prop: uniqueSymbol,
};
const genericFactory = (value) => ({ prop: value });
const baz = genericFactory(uniqueSymbol);
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

Activity

  1. RyanCavanaugh commented on Feb 20, 2020

    @RyanCavanaugh
    Member

    Ron Buckton (@rbuckton) I believe this is the intended behavior, but can you provide context?

  2. weswigham commented on Feb 20, 2020

    @weswigham
    Member

    Same is true of other literal types:

    const uniqueSymbol = "tag"
    
    const foo = {
      prop: uniqueSymbol,
    }
    
    type Foo = typeof foo 

    prop is 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).

  3. pmeller commented on Feb 20, 2020

    @pmeller
    Author

    Taking 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,
      }
    }
  4. pmeller commented on Feb 20, 2020

    @pmeller
    Author

    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 }

    Playground link

  5. pmeller commented on Feb 20, 2020

    @pmeller
    Author

    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 symbol that 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,
    }

    Playground link

    Both as const casting and explicit type declaration cannot be used on symbols.

  6. AlnisS-ptc commented on Feb 1, 2025

    @AlnisS-ptc

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions