Typescript: Allow unique symbols to be interchangeable with types. Also assignability bug with `Symbol()`.

Created on 26 Dec 2017  Β·  31Comments  Β·  Source: microsoft/TypeScript

Now that we have #15473, when a symbolβ€”_which is guaranteed to be unique by the ES2015 spec_β€”is assigned to a const variable, it can be used (via computed property syntax) in place of a normal property name.

Quick contrived example:

export const FOO = Symbol('FOO');
export const BAR = Symbol('BAR');

export interface Thing {
    [ FOO ]: boolean;
    [ BAR ]: number;
}

const thing: Thing = undefined;
const foo = thing[ FOO ]; // ☝️ hover shows type of `foo` as `boolean`
const bar = thing[ BAR ]; // ☝️ hover shows type of `bar` as `number`

I love this, it allows a symbol to act as a unique identifier. But it truth, a symbol represents a unique value, I want to use that unique value to describe a type.

Another quick contrived example:

export const FOO = Symbol('FOO');
export const BAR = Symbol('BAR');

export function fooOrBar(value: FOO | BAR): string { // ❌ Cannot find name 'FOO'.
                                                     // ❌ Cannot find name 'BAR'.
    if (value === FOO) return 'You gave me FOO!';
    else if (value === BAR) return 'You gave me BAR!';
    else throw TypeError('What was that!?');
}

Is that something anyone else would want? A lot of us will use a symbol to define a unique constant, I think it just makes sense to be able to use that constant to... refer to that constant.


On a side note, I think I may have found a bug. So while typing up this issue, I found that I am able to achieve the desired behavior by using an intersection with a "tagged" object literal:

export type FOO = symbol & { FOO: true };
export const FOO: FOO = Symbol('FOO'); // ❓️ no error
export type BAR = symbol & { BAR: true };
export const BAR: BAR = Symbol('BAR'); // ❓ no error

While this is nice for me because it achieves what I want, I don't think it should work. Heres why:

So if you hover over the Symbol() constructor you'll see this:

var Symbol: SymbolConstructor
(description?: string | number) => symbol

That's what I expected to see. You (optionally) give it a string | number description and it gives you a symbol in return. So why doesn't the compiler freakout when I assign it to something that is symbol & { FOO: true }? If IRRC the spec says that no properties can be set on a symbol. I can't find where it said that, but really quickly in my DevTools Console, I did this which seems to affirm my belief:

$> const foo = Symbol('foo')
undefined
$> foo
Symbol(foo)
$> foo.foo = true
true
$> foo
Symbol(foo)
$> foo.foo
undefined

Perhaps there some special assignability feature for typescript primitive symbol that I'm overlooking? I don't know. But if you do:

export type FOO = string & { FOO: true };
export const FOO: FOO = String('FOO'); // ❌ Type 'string' is not assignable to type 'FOO'.
                                       // ❌ Type 'string' is not assignable to type '{ FOO: true; }'.

Looking at the type info for the String() constructor, it's nearly identical:

const String: StringConstructor
(value?: any) => string

so why does it behave differently?


Just out of curiosity, I tried:

export const FOO: boolean = Symbol('FOO');

And got no errors. Something is definitely broken because symbol is acting like any.

Because you will ask, I am currently running: [email protected], I've also tested this on 2.7.0-insiders.20171214, but the playground correctly gives me errors.

Needs More Info

All 31 comments

Just to add to the mystery a little.

export const FOO: FOO = Symbol('FOO') as symbol; //  Type 'symbol' is not assignable to type 'FOO'.
                                                 // Type 'symbol' is not assignable to type '{ FOO: true; }'.

And this one is a little crazy!

const NEVER: never = Symbol("Never?");

Type of NEVER is now symbol! Symbol() is quite powerful!

You can reference the type of the constant with typeof (the type query operator) in the following way:

const Foo = Symbol("foo");
type Foo = typeof foo;

@DanielRosenwasser not sure if we're on the same page here...

typeof Symbol('foo') // => symbol

I'm looking for the type of a symbol assigned to a constant to be mutually exclusive from other symbols.

_Desired Behavior:_

export const FOO = Symbol('FOO');
export const BAR = Symbol('BAR');

export function onlyFOO(value: FOO): void {
   // ...
}

onlyFOO(FOO); // βœ”οΈ 
onlyFOO(BAR): // ❌ Type 'BAR' is not assignable to type 'FOO'.

okay, I went back and looked at the new unique keyword, and using that I was able to get what I wanted:

export const FOO: unique symbol = Symbol('FOO');
export const BAR: unique symbol = Symbol('BAR');

export function onlyFOO(value: typeof FOO): void {
   // ...
}

onlyFOO(FOO); // βœ”οΈ 
onlyFOO(BAR): // ❌ Type 'unique symbol' is not assignable to type 'unique symbol'.

The error message: "Type 'unique symbol' is not assignable to type 'unique symbol'." sucks though. Would be nice if it contained the identifiers.

I thought perhaps defining a type alias for the type of the variable like so:

export const FOO: unique symbol = Symbol('FOO');
export type FOO = typeof FOO;
export const BAR: unique symbol = Symbol('BAR');
export type BAR = typeof BAR;

might cause the alias to show up in the assignment error message, but alas, it does not.


When I read #15473, I was thinking that because the declaration output of export const FOO = Symbol('FOO'); is export const FOO: unique symbol; therefore a const variable assignment where the right-hand side was simply the Symbol() constructor (ie: const FOO = Symbol('FOO');) would have the infered type of unique symbol.

I see why this isn't necessarily the case: in a declaration, you don't see the right-hand side of an assignment therefor the variable has to be explicitly marked as being unique.

I also expected to be able to use the constant as a type in a similar way to string literals and so I think this pattern will be very common:

export const FOO: unique symbol = Symbol('FOO');
export type FOO = typeof FOO;

So common that I think it deserves some sugar to reduce the boilerplate. What do folks think about this?

export const type FOO = Symbol('FOO');

I don't see why such a declaration wouldn't also work for string and integer literals.

That would be so damn useful for typing redux actions!!

This is what I'm currently doing for any interested:


Expand to View (ommitted because it's super long... because it's redux)

./src/common/state/ui/share/actions.ts

import { Shareable } from './state';
import { Action } from 'common/state/KnownActions';

export type SHARE = 'ui/share::SHARE';
export const SHARE: SHARE = 'ui/share::SHARE';

export interface SHARE_Action extends Action<SHARE> {
    type: SHARE;
    payload: Shareable;
}

export function share(shareable: Shareable): SHARE_Action {
    return {
        type: SHARE,
        payload: shareable
    };
}

export type DISMISS = 'ui/share::DISMISS';
export const DISMISS: DISMISS = 'ui/share::DISMISS';

export interface DISMISS_Action extends Action<DISMISS> {
    type: DISMISS;
}

export function dismiss(): DISMISS_Action {
    return {
        type: DISMISS,
    };
}

export type UIShareActions = (
    | SHARE_Action
    | DISMISS_Action
);

export type UIShareActionIDs = (
    | SHARE
    | DISMISS
);

export default {
    SHARE,
    share,
    DISMISS,
    dismiss
};

./src/common/state/KnownActions.ts

export interface Action<T extends string> extends Action { // extends `redux`'s `Action` interface
    type: T;
    payload?: any;
    error?: Error;
    meta?: any;
}

export type KnownActions = (
    | SystemActions
    | UIActions
);

export type KnownActionsIDs = (
    | SystemActionIDs
    | UIActionIDs
);

super verbose, but has perfect type checking/completions

I am not sure i understand the issue really.. the examples you are referring to the type FOO as a unique symbol are working if you use typeof FOO instead.

@mhegazy the issue is that for symbols, the constant it is assigned to is, functionally, the literal form of that value. For all other literal forms typeof is not required (E.g. fn(s: typeof 'my literal)') and this makes for an incongruous experience when working with unique symbol types.

the issue is that for symbols, the constant it is assigned to is, functionally, the literal form of that value. For all other literal forms typeof is not required (E.g. fn(s: typeof 'my literal)') and this makes for an incongruous experience when working with unique symbol types.

That is not totally accurate.. for the other literal types, you use the literal text (be it string, number, boolean) to identify the types and these are unambiguous in this location.

Enum literal types are similar in the sense that they are identifier names, but for these we have made sure that you can not merge other declarations with them in the past, so we were free to reuse their names as type names.

For const declarations that is a different story. you can today merge const C = Symbol(); and type C = number; and C in a type position has a well defined meaning. using the same variable name as a type is first a breaking change, and second is not inline with how declaration spaces work in TS and how merging work. see https://www.typescriptlang.org/docs/handbook/declaration-merging.html for more info.

@mhegazy I wasn't trying to explain how typescript is implemented or how the typesystem works. I was just saying that as a developer, string and integer literals have the property of being their own type, but that there's no such convenient literal syntax for a unique symbol. But because one of the main use cases for unique symbols is to define object properties with them, there's a need to use typeof a lot or to always define a type type C = typeof C merging the type name with the constant. My suggestion is just to reduce boilerplate by allowing that to be done with a single statement.

For const declarations that is a different story. you can today merge const C = Symbol(); and type C = number; and C in a type position has a well defined meaning. using the same variable name as a type is first a breaking change, and second is not inline with how declaration spaces work in TS and how merging work. see https://www.typescriptlang.org/docs/handbook/declaration-merging.html for more info.

@mhegazy I’m not sure how repeating yourself with no additional information is helpful here.

The suggestion you provide here, if implemented, would be a breaking change. today types and values with the same name already have a meaning.. this suggestion would change that meaning.. I would recommend reading the link i have shared.

How would this be a _breaking_ change? What @chriseppstein proposes currently is a syntax error. Assuming this were implemented, the use of const type FOO would signal the author's intent to opt-into this departure from the normal declaration merging logic, any existing code would continue to work as is. Ergo, this is not a _breaking_ change. Just new, optional, functionally.

So

export const type FOO = Symbol('FOO');
export const type BAR = 'BAR';

Would be functionally equivilant to

export const FOO: unique symbol = Symbol('FOO');
export type FOO = typeof FOO;
export const BAR: 'BAR' = 'BAR';
export type BAR = typeof BAR;

How would this be a breaking change?

In the OP, you have used export const FOO = Symbol('FOO'); to declare a type FOO. and that would be a breaking change.

We have talked about allowing a tagged symbol type, though that is slightly different from what the current feature does. that would be something we can consider. Please share some supporting scenarios why const s = symbol() is not sufficient.

@mhegazy I think you misread my proposal. It was to add syntactic sugar to allow declaring a constant and a type of the same name with a single statement where the type is automatically defined as typeof <the constant>. I agree that declaring a type automatically would be a breaking change, which is why I suggested const type but I'm not wedded to any particular syntax. As @rozzzly points out, my proposal can certainly be implemented in a way as to avoid any chance of a breaking change if so desired through selection of syntax that is not currently valid.

Please share some supporting scenarios why const s = symbol() is not sufficient.

A primary use case for symbols is to define an unambiguous object property that is guaranteed to not conflict when an object is extended by arbitrary code. In this case, the type of that well known property is always typeof <some constant> and so that property needs to be declared in interfaces and classes accordingly. The requirement to frequently type typeof for symbols or to consistently declare a type of the same name as the constant, is incongruous with string and number literal types which are their own type. This proposal removes boilerplate code and makes symbol literals work as class/interface field types more seamlessly.

A primary use case for symbols is to define an unambiguous object property that is guaranteed to not conflict when an object is extended by arbitrary code.

how do you expect your API users would use the symbol? through the constant or through Symbol.for("somekey")?

The requirement to frequently type typeof for symbols or to consistently declare a type of the same name as the constant, is incongruous with string and number literal types which are their own type.

Can you elaborate on why you need to write typeof multiple times? why not just use the const directelly?

how do you expect your API users would use the symbol? through the constant or through Symbol.for("somekey")?

The constant. I never used Symbol.for() in any example and the limitations of Symbol.for() for interface definitions are already well documented with this new feature.

Can you elaborate on why you need to write typeof multiple times? why not just use the const directelly?

I'm actually not sure if you're trolling me here. This issue starts with "when trying to use the const directly it gives me an error" to which @DanielRosenwasser replied (https://github.com/Microsoft/TypeScript/issues/20898#issuecomment-354073352)

(paraphrasing) use this boilerplate:

const Foo = Symbol("foo");
type Foo = typeof foo;

To which I replied, (paraphrasing) "hey, what if we just could type that with one line instead of two."

Why are we going around in circles here?

I'm actually not sure if you're trolling me here.

I am not. i am genuinely trying to understand the context for this request. all the code samples in this thread are just snippets. No one put this request in the context of a use case.. like i am not sure i understand what your API is doing, and why you need to write typeof Foo that many times.. it seems to me most of the time you are writing Foo either in an interface declaration interface i { [Foo] :string } or in property access o[Foo]. in the rare occasion that you want users to pass you an object, and a symbol to use to index into it, why is not typeof Foo too verbose? is there a scenario you are trying to achieve that makes it a must to keep writing typeof Foo? or is it just one place in your API? are you looking for a symbol enum instead (tracked by https://github.com/Microsoft/TypeScript/issues/18408)?

@rozzzly's comment seems to suggest he wants a tagged symbol type, which lends itself to symbol.for(), and this one i understand, but i am not sure i understand the other one.

why is typeof Foo too verbose?

As I've said, It's more verbose than string and integer literals and incongruous with the dev experience of using those types. It's not a big deal, I just think this makes the code marginally nicer to use and read.

is there a scenario you are trying to achieve that makes it a must to keep writing typeof Foo?

On a per-symbol basis I do not think that there will be very many uses of typeof, but if you have some code that is working with even 3-5 symbols per interface and you have a few such interfaces, it starts to look messy and have a lot of boilerplate in aggregate.

are you looking for a symbol enum instead?

Nope. I'm just trying to make some interfaces and classes that have some symbol properties.

which lends itself to Symbol.for()

It's my understanding that with this feature, at this time, multiple calls to with the same string to Symbol.for() are not understood to be the same unique symbol by the type checker and that a single constant must be used to get unique symbol type checking to work. As such, I did not personally, consider the distinction important at this time. Personally, I'd love to see a literal syntax to replace Symbol.for() not unlike what ruby has with :foo... but I don't consider that to be on-topic for this issue.

Nope. I'm just trying to make some interfaces and classes that have some symbol properties.

still confused, so why isn't it just:

const sym = Symbol();

interface I {
    [sym]: string;
}
class C implements I {
    [sym]: string;
}

@mhegazy Ah, that's fair. sorry, I got my story a bit mixed up here. In the code I was working on when I bumped into this, I was trying to create a discriminator property from symbols instead of strings.

export const NODE_TYPE = Symbol("Node Type");
export const A = Symbol("Node A");
export const B = Symbol("Node B");
export const C = Symbol("Node C");
export interface NodeA {
  [NODE_TYPE]: typeof A;
}
export interface NodeB {
  [NODE_TYPE]: typeof B;
}
export interface NodeC {
  [NODE_TYPE]: typeof C;
}
export type Node = NodeA | NodeB | NodeC;

Whereas when this code was using a string type it was like so:

export interface NodeA {
  node_type: 'a';
}
export interface NodeB {
  node_type: 'b';
}
export interface NodeC {
  node_type: 'c';
}
export type Node = NodeA | NodeB | NodeC;

I found the need to use typeof off putting and would prefer:

export const NODE_TYPE = Symbol("Node Type");
export const type A = Symbol("Node A");
export const type B = Symbol("Node B");
export const type C = Symbol("Node C");
export interface NodeA {
  [NODE_TYPE]: A;
}
export interface NodeB {
  [NODE_TYPE]: B;
}
export interface NodeC {
  [NODE_TYPE]: C;
}
export type Node = NodeA | NodeB | NodeC;

Not to jump in from a weaker understanding of the topics under discussion, but if we are proposing enhancing syntax in this way, would we also consider having an enum where the values are Symbols?

Not to jump in from a weaker understanding of the topics under discussion, but if we are proposing enhancing syntax in this way, would we also consider having an enum where the values are Symbols?

Please see #18408

@chriseppstein Thanks for the explanation. Seems you need access to both the type and the value. ( i am assuming somewhere you have if (node[Node_Type] === A) ... and thus the solution proposed by @rozzzly would not fit your use case. type A = Symbol("Node A"); does not generate any code. I do not think we want to add a new syntax (const type ..) just for symbols that makes a type generate code. we could discuss it, but we have discussed similar proposals in the past and concluded we do not want to do that.
I would say if we were going to take on @rozzzly's proposal, it would be in the type space only. i.e. you will have to cast your way around const A = <symbol("Node A")><symbol>Symbol("Node A"); which a'int pretty.

I think a symbol enum is the best solution here. here are the reasons,

  • enums give you access to both value and type
  • enum members are themselves type names, so you do not need to write typeof E.A, just E.A.
  • enums give you the union type automatically, so no need for Node there

@mhegazy I guess it would work, but you'd have to augment the symbol enum across modules with additional values for cases where you have an extensible set of types, which is also kind of annoying. For my current use cases, it would be fine though.

Does any language have such extensible enums? @chriseppstein?

@sylvanaar You can extend enums in TS πŸ€·β€β™‚οΈ

enum Foo { a, b, c }
enum Foo { d = 3, e, f}

So I assume that you can do so across modules using module augmentation.

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

Was this page helpful?
0 / 5 - 0 ratings