Closed wereHamster closed 3 years ago
That is part of the ES6 Symbol support we @JsonFreeman is working on. Your code sample should be supported in the next release.
@wereHamster, with pull request #1978, this should become legal, and obj[theAnswer]
will have type any
. Is that sufficient for what you are looking for, or do you need stronger typing?
Will it be possible to specify the type of properties which are indexed by symbols? Something like the following:
var theAnswer = Symbol('secret');
interface DeepThought {
[theAnswer]: number;
}
Based on the comments in that PR, no:
This does not cover symbol indexers, which allows an object to act as a map with arbitrary symbol keys.
I think @wereHamster is talking about a stronger typing than @danquirk. There are 3 levels of support here. The most basic level is provided by my PR, but that is just for symbols that are properties of the global Symbol object, not user defined symbols. So,
var theAnswer = Symbol('secret');
interface DeepThought {
[Symbol.toStringTag](): string; // Allowed
[theAnswer]: number; // not allowed
}
The next level of support would be to allow a symbol indexer:
var theAnswer = Symbol('secret');
interface DeepThought {
[s: symbol]: number;
}
var d: DeepThought;
d[theAnswer] = 42; // Typed as number
This is on our radar, and can be implemented easily.
The strongest level is what you're asking for, which is something like:
var theAnswer = Symbol('secret');
var theQuestion = Symbol('secret');
interface DeepThought {
[theQuestion]: string;
[theAnswer]: number;
}
var d: DeepThought;
d[theQuesiton] = "why";
d[theAnswer] = 42;
This would be really nice, but so far we have not come up with a sensible design for it. It ultimately seems to hinge on making the type depend on the runtime value of these symbols. We will continue to think about it, as it is clearly a useful thing to do.
With my PR, you should at least be able to use a symbol to pull a value out of an object. It will be any
, but you will no longer get an error.
@wereHamster I did a little writeup #2012 that you may be interested in.
I've merged request #1978, but I will leave this bug open, as it seems to ask for more than I provided with that change. However, with my change, the original error will go away.
@wereHamster can you post an update of what more you'd like to see happen here? Wasn't immediately clear to me what we have implemented vs what you posted
Any idea when symbol
will be valid type as an indexer? Is this something that could be done as a community PR?
We would take a PR for this. @JsonFreeman can provide details on some of the issues that you might run into.
I actually think adding a symbol indexer would be pretty straightforward. It would work just like number and string, except that it wouldn't be compatible with either of them in assignability, type argument inference, etc. The main challenge is just making sure you remember to add logic in all the appropriate places.
@RyanCavanaugh, it would be nice to eventually have the last example in https://github.com/Microsoft/TypeScript/issues/1863#issuecomment-73668456 typecheck. But if you prefer you can split this issue up into multiple smaller issues which build on top of each other.
Was there any update on this front? AFAIU the latest version of the compiler supports only the first level described in https://github.com/Microsoft/TypeScript/issues/1863#issuecomment-73668456.
We would be happy to accept PRs for this change.
It might be worth tracking the two levels as two separate issues. The indexers seem fairly straightforward, but the utility is not clear. The full support with constant tracking seems quite difficult, but probably more useful.
Constant tracking is already tracked in https://github.com/Microsoft/TypeScript/issues/5579. this issue is for adding support for a symbol indexer, similar to string and numberic indexers.
Got it, makes sense.
@JsonFreeman @mhegazy an issue is available at #12932
Just thought I'd throw my use case into the ring. I'm writing a tool that allows queries to be described by specifying plain text keys for matching against arbitrary object properties, and symbols for specifying matching operators. By using symbols for well-known operators, I avoid the ambiguity of matching an operator versus a field whose key is the same as that of the well-known operator.
Because symbols can't be specified as index keys, in contrast to what JavaScript explicitly allows, I'm forced to cast to <any>
in a number of places, which degrades the code quality.
interface Query {
[key: string|symbol]: any;
}
const Q = {
startsWith: Symbol('startsWith'),
gte: Symbol('gte'),
lte: Symbol('lte')
}
const sample: Query = {
name: {
[Q.startsWith]: 'M',
length: {
[Q.lte]: 25
}
},
age: {
[Q.gte]: 18
}
};
Use of "unlikely" first characters such as a $
character is not a suitable compromise, given the variety of data that the query engine may need to inspect.
Hi guys. Is there any movement in this? I need it so I would be happy to contribute the necessary changes. Haven't contributed to TS before though.
@mhegazy @RyanCavanaugh I know you guys are incredibly busy, but could you weigh in when you get a chance? Symbols are a really important tool for architecting libraries and frameworks, and the lack of ability to use them in interfaces is a definite pain point.
I'm asking is there anything in progress? Sincerely hope this feature be supported.
https://github.com/Microsoft/TypeScript/pull/15473 looks related.
Yeah still looking for this today, this is what I see in Webstorm:
var test: symbol = Symbol();
const x = {
[test]: 1
};
x[test];
console.log(x[test]);
console.log(x['test']);
but the type of x
is not right, being inferred as
{
[key: string]: number
}
Yeah still looking for this today, this is what I see in Webstorm:
Please note that JetBrains' own language service that is enabled by default in WebStorm, intelliJ IDEA, and so on.
This works in TS 2.7
const key = Symbol('key')
const a: { [key]?: number } = {}
a[key] = 5
Any update on this?
My problem:
export interface Dict<T> {
[index: string]: T;
[index: number]: T;
}
const keyMap: Dict<number> = {};
function set<T extends object>(index: keyof T) {
keyMap[index] = 1; // Error Type 'keyof T' cannot be used to index type 'Dict<number>'
}
But this also does not work, because symbol cannot be an index type.
export interface Dict<T> {
[index: string]: T;
[index: symbol]: T; // Error: An index signature parameter type must be 'string' or 'number'
[index: number]: T;
}
Expected behavior:
symbol
should be a valid index type
Actual behavior:
symbol
is not a valid index type
Using the workaround with casting as string | number
seems for me very bad.
How is util.promisify.custom
supposed to be used in TypeScript? It seems that using constant symbols is supported now, but only if they are explicitly defined. So this is valid TypeScript (aside from f
not being initialized):
const custom = Symbol()
interface PromisifyCustom<T, TResult> extends Function {
[custom](param: T): Promise<TResult>
}
const f: PromisifyCustom<string, void>
f[custom] = str => Promise.resolve()
But if promisify.custom
is used instead of custom
, the attempt to reference f[promisify.custom]
results in the error Element implicitly has an 'any' type because type 'PromisifyCustom<string, void>' has no index signature.
:
import {promisify} from 'util'
interface PromisifyCustom<T, TResult> extends Function {
[promisify.custom](param: T): Promise<TResult>
}
const f: PromisifyCustom<string, void>
f[promisify.custom] = str => Promise.resolve()
I would like to assign to a function's promisify.custom
field, but it seems (given the behavior described above) that the only way to do this is to cast the function to an any
type.
I cannot understand why symbol is not allowed as key index, below code should works and is accepted by Typescript 2.8 but it is not allowed by Typescript 2.9
/**
* Key can only be number, string or symbol
* */
export class SimpleMapMap<K extends PropertyKey, V> {
private o: { [k: string ]: V } = {};
public has (k: K): boolean {
return k in this.o;
}
public get (k: K): V {
return this.o[k as PropertyKey];
}
public set (k: K, v: V) {
this.o[k as PropertyKey] = v;
}
public getMap (k: K): V {
if (k in this.o) {
return this.o[k as PropertyKey];
}
const res = new SimpleMapMap<K, V>();
this.o[k as PropertyKey] = res as any as V;
return res as any as V;
}
public clear () {
this.o = {};
}
}
I tried below, which is more 'correct' to me but it is not accepted by both version of Typescript compiler
/**
* Key can only be number, string or symbol
* */
export class SimpleMapMap<K extends PropertyKey, V> {
private o: { [k: K ]: V } = {};
public has (k: K): boolean {
return k in this.o;
}
public get (k: K): V {
return this.o[k];
}
public set (k: K, v: V) {
this.o[k] = v;
}
public getMap (k: K): V {
if (k in this.o) {
return this.o[k];
}
const res = new SimpleMapMap<K, V>();
this.o[k as PropertyKey] = res as any as V;
return res as any as V;
}
public clear () {
this.o = {};
}
}
The status of this ticket indicates that what you are suggesting is the desired behaviour, but the core team is not at this point committing resources to add this feature enhancement, they are opening it up to the community to address.
@beenotung Although this is not an ideal solution, assuming that the class you posted is the only place where you need such a behaviour, you can do unsafe casts inside the class, but keeping the class and methods signatures the same, so that the consumers of the class won't see that:
/**
* Key can only be number, string or symbol
* */
export class SimpleMapMap<K extends PropertyKey, V> {
private o: { [k: string]: V } = {};
public has(k: K): boolean {
return k in this.o;
}
public get(k: K): V {
return this.o[k as any];
}
public set(k: K, v: V) {
this.o[k as any] = v;
}
public getMap(k: K): V {
if (k in this.o) {
return this.o[k as any];
}
const res = new SimpleMapMap<K, V>();
this.o[k as any] = res as any as V;
return res as any as V;
}
public clear() {
this.o = {};
}
}
Because the signatures are the same, whenever you use this class, you will have the type validation applied correctly, and, when this issue is solved, you will just need to change this class (it will be transparent to the consumers).
An example of a consumer is like bellow (the code won't need any change when this issue is fixed):
const s1 = Symbol(1);
const s2 = Symbol(2);
let m = new SimpleMapMap<symbol, number>()
m.set(s1, 1);
m.set(s2, 2);
m.get(s1);
m.get(1); //error
Typescript 3.0.1, got bitten by this.
I want a record that accepts symbol
but TS won't let me.
It's been 3.5 years since this issue was opened, can we have symbols now, please 🙏
The irony is that TS contradict itself.
TS expands keyof any = number | string | symbol
.
But then when you do record[symbol]
TS refuses saying
Type 'symbol' cannot be used as an indexer.
Yeah I have been suffering with this one for awhile sadly, my latest question regarding this topic:
@RyanCavanaugh @DanielRosenwasser @mhegazy Any updates? This issue is nearing its fourth birthday.
If someone could point me in the right direction, I can give it a go. If there's tests to match, even better.
@jhpratt there's a PR up at #26797 (note the caveat about well-known symbols). There are recent design meeting notes about it in #28581 (but no resolution recorded there). There's a bit more feedback about why that PR is held up here. It seems to be regarded as a fringe/low impact issue, so maybe more upvotes on the PR may help raise the profile of the issue.
Thanks @yortus. I've just asked Ryan if the PR is still planned for 3.2, which is what is indicated by the milestone. Hopefully that's the case, and this will be resolved!
The PR pointed by @yortus seems a huge change, Shouldn't the fix for this bug be very minor? e.g. adding an or statement in the condition check. (I have not located the place to change yet.)
temp solution here https://github.com/Microsoft/TypeScript/issues/24587#issuecomment-412287117, kinda ugly but gets the job done
const DEFAULT_LEVEL: string = Symbol("__default__") as any;
another https://github.com/Microsoft/TypeScript/issues/24587#issuecomment-460650063, since linters h8 any
const ItemId: string = Symbol('Item.Id') as unknown as string;
type Item = Record<string, string>;
const shoes: Item = {
name: 'whatever',
}
shoes[ItemId] = 'randomlygeneratedstring'; // no error
{ name: 'whatever', [Symbol(Item.Id)]: 'randomlygeneratedstring' }
i guess one of the gotchas i've noticed in using symbols though is if you have a project involving the child_process module, yes you can share types/enums/interfaces between the two process but never symbols.
it's really great to have this solved though, symbols are really great in tracking objects without polluting their keys and being required to use maps/sets, on top of that the benchmarks in the recent years show that accessing symbols are just as fast as accessing string / number keys
Edit: Turns out this approach is only works with Record<X,Y>
but not Interfaces
. Ended up using // @ts-ignore
for now since it's still a syntactically correct and still compiles well to JS just as it should.
A thing to note though is when using // @ts-ignore
on lines involving symbols, it's actually possible (and helps) to manually specify the type of that symbol. VSCode still kind of picks up on it.
const id = Symbol('ID');
interface User {
name: string;
age: number;
}
const alice: User = {
name: 'alice',
age: 25,
};
// @ts-ignore
alice[id] = 'maybeSomeUUIDv4String';
// ...
// then somewhere, when you need this User's id
// @ts-ignore
const id: string = alice[id];
console.log(id); // here you can hover on id and it will say it's a string
Don't know, if anyone has started something to fix this, but if not, I have now.
My time is limited, though and I have zero knowledge about the Typescript sources. I've made a fork (https://github.com/Neonit/TypeScript), but no pull request, yet, because I don't want to molest the devs with unfinished (?) changes. I would ask everyone to contribute what they can to my fork. I will eventually issue a PR then.
So far I have found a way to fix the interface index type restriction. I don't know, if there's more to it. I was able to index an object with a symbol in TS 3.4 without any fixes. (https://www.typescriptlang.org/play/#src=const%20o%20%3D%20%7B%7D%3B%0D%0Aconst%20s%20%3D%20Symbol('s')%3B%0D%0A%0D%0Ao%5Bs%5D%20%3D%20123%3B)
Have a look at my commit to see what I found: https://github.com/Neonit/TypeScript-SymbolKeys/commit/11cb7c13c2494ff32cdec2d4f82673058c825dc3
Missing:
I hope this will get things started finally after years of waiting.
the fix looks good. can TypeScript dev take a look on it?
hello, any progress for this?
Just opened a SO thread about this: https://stackoverflow.com/questions/59118271/using-symbol-as-object-key-type-in-typescript
Why isn't that possible? Isn't symbol
another primitive type like number
- so why's there a difference?
Hello, any progress for this?
FIVE years have been passed!
You're not gonna believe how long it took C++ to get closures 😲
lol fair, but C++ isn't marketing itself as a superset of a language that has closures :-p
@ljharb keep beating that horse, it's still twitching 😛
For those who are targeting newer runtimes, why not use a Map
? I've anecdotally found that a lot of developers don't know Map
s exist, so I'm curious whether there's another scenario that I'm missing.
let m = new Map<symbol, number>();
let s = Symbol("arbitrary symbol!");
m.set(s, 1000);
let a = m.get(s);
Maps and objects have different use cases.
@DanielRosenwasser well-known symbols are used as protocols; a Map key of Symbol.match
, for example, won't make the object RegExp-like (and any object may want a Symbol.iterable
key to make it iterable without having to explicitly use TS built-in Iterable types).
TypeScript now has a ES6 target mode which includes definitions
Symbol
. However when trying to index an object with a symbol, I get an error (An index expression argument must be of type 'string', 'number', or 'any').