-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Allow unique symbols to be interchangeable with types. Also assignability bug with Symbol()
.
#20898
Comments
Just to add to the mystery a little.
And this one is a little crazy!
Type of |
You can reference the type of the constant with 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 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 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 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 |
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 |
@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 |
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 |
@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 |
|
@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 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; |
In the OP, you have used 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 |
@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 |
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 |
how do you expect your API users would use the symbol? through the constant or through |
Can you elaborate on why you need to write |
The constant. I never used
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 (#20898 (comment)) (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 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 @rozzzly's comment seems to suggest he wants a tagged symbol type, which lends itself to |
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.
On a per-symbol basis I do not think that there will be very many uses of
Nope. I'm just trying to make some interfaces and classes that have some symbol properties.
It's my understanding that with this feature, at this time, multiple calls to with the same string to |
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 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 |
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 I think a symbol enum is the best solution here. here are the reasons,
|
@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. |
Now that we have #15473, when a
symbol
—which is guaranteed to be unique by the ES2015 spec—is assigned to aconst
variable, it can be used (via computed property syntax) in place of a normal property name.Quick contrived example:
I love this, it allows a
symbol
to act as a unique identifier. But it truth, asymbol
represents a unique value, I want to use that unique value to describe a type.Another quick contrived example:
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:
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:That's what I expected to see. You (optionally) give it a
string | number
description and it gives you asymbol
in return. So why doesn't the compiler freakout when I assign it to something that issymbol & { FOO: true }
? If IRRC the spec says that no properties can be set on asymbol
. I can't find where it said that, but really quickly in my DevTools Console, I did this which seems to affirm my belief:Perhaps there some special assignability feature for typescript primitive
symbol
that I'm overlooking? I don't know. But if you do:Looking at the type info for the
String()
constructor, it's nearly identical:so why does it behave differently?
Just out of curiosity, I tried:
And got no errors. Something is definitely broken because
symbol
is acting likeany
.Because you will ask, I am currently running:
[email protected]
, I've also tested this on2.7.0-insiders.20171214
, but the playground correctly gives me errors.The text was updated successfully, but these errors were encountered: