Skip to content

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

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

Open
pmeller opened this issue Feb 19, 2020 · 6 comments
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@pmeller
Copy link

pmeller commented Feb 19, 2020

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

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Feb 20, 2020
@RyanCavanaugh
Copy link
Member

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

@weswigham
Copy link
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).

@pmeller
Copy link
Author

pmeller commented Feb 20, 2020

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

@pmeller
Copy link
Author

pmeller commented Feb 20, 2020

@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

@pmeller
Copy link
Author

pmeller commented Feb 20, 2020

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.

@AlnisS-ptc
Copy link

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
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

5 participants