Skip to content
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 indexing with symbols #1863

Closed
wereHamster opened this issue Jan 30, 2015 · 103 comments · Fixed by #44512
Closed

Allow indexing with symbols #1863

wereHamster opened this issue Jan 30, 2015 · 103 comments · Fixed by #44512
Labels
Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Fix Available A PR has been opened for this issue Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@wereHamster
Copy link

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').

var theAnswer = Symbol('secret');
var obj = {};
obj[theAnswer] = 42; // Currently error, but should be allowed
@mhegazy
Copy link
Contributor

mhegazy commented Jan 30, 2015

That is part of the ES6 Symbol support we @JsonFreeman is working on. Your code sample should be supported in the next release.

@JsonFreeman
Copy link
Contributor

@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?

@wereHamster
Copy link
Author

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

@danquirk
Copy link
Member

danquirk commented Feb 9, 2015

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.

@JsonFreeman
Copy link
Contributor

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.

@JsonFreeman
Copy link
Contributor

@wereHamster I did a little writeup #2012 that you may be interested in.

@JsonFreeman
Copy link
Contributor

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.

@mhegazy mhegazy added the In Discussion Not yet reached consensus label Jul 27, 2015
@RyanCavanaugh RyanCavanaugh added Needs More Info The issue still hasn't been fully clarified and removed In Discussion Not yet reached consensus labels Oct 5, 2015
@RyanCavanaugh
Copy link
Member

@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

@kitsonk
Copy link
Contributor

kitsonk commented Dec 31, 2015

Any idea when symbol will be valid type as an indexer? Is this something that could be done as a community PR?

@mhegazy mhegazy added In Discussion Not yet reached consensus and removed Needs More Info The issue still hasn't been fully clarified labels Jan 5, 2016
@mhegazy
Copy link
Contributor

mhegazy commented Jan 5, 2016

We would take a PR for this. @JsonFreeman can provide details on some of the issues that you might run into.

@JsonFreeman
Copy link
Contributor

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.

@mhegazy mhegazy added Help Wanted You can do this and removed In Discussion Not yet reached consensus labels Jan 5, 2016
@wereHamster
Copy link
Author

@RyanCavanaugh, it would be nice to eventually have the last example in #1863 (comment) typecheck. But if you prefer you can split this issue up into multiple smaller issues which build on top of each other.

@MingweiSamuel
Copy link

MingweiSamuel commented Aug 8, 2020

A workaround is to use generic function to assign value ...

var theAnswer: symbol = Symbol('secret');
var obj = {} as Record<symbol, number>;
obj[theAnswer] = 42; // Currently error, but should be allowed

Object.assign(obj, {theAnswer: 42}) // allowed

You're looking for

Objet.assign(obj, { [theAnswer]: 42 });

However there isn't a way to read x[theAnswer] back out without a cast AFAIK see comment two below

@leidegre
Copy link

leidegre commented Sep 8, 2020

For the love of God, please make this a priority.

@beenotung
Copy link

You're looking for

Objet.assign(obj, { [theAnswer]: 42 });

However there isn't a way to read x[theAnswer] back out without a cast AFAIK

As pointed out by mellonis and MingweiSamuel, the workarounds using generic function are:

var theAnswer: symbol = Symbol("secret");
var obj = {} as Record<symbol, number>;

obj[theAnswer] = 42; // Not allowed, but should be allowed

Object.assign(obj, { [theAnswer]: 42 }); // allowed

function get<T, K extends keyof T>(object: T, key: K): T[K] {
  return object[key];
}

var value = obj[theAnswer]; // Not allowed, but should be allowed

var value = get(obj, theAnswer); // allowed

@james4388
Copy link

Five years and Symbol as index still not allowed

@james4388
Copy link

james4388 commented Oct 16, 2020

Found a work-around on this case, it not generic but work in some case:

const SYMKEY = Symbol.for('my-key');

interface MyObject {   // Original object interface
  key: string
}

interface MyObjectExtended extends MyObject {
  [SYMKEY]?: string
}

const myObj: MyObject = {
  'key': 'value'
}

// myObj[SYMKEY] = '???' // Not allowed

function getValue(obj: MyObjectExtended, key: keyof MyObjectExtended): any {
  return obj[key];
}

function setValue(obj: MyObjectExtended, key: keyof MyObjectExtended, value: any): void {
  obj[key] = value
}

setValue(myObj, SYMKEY, 'Hello world');
console.log(getValue(myObj, SYMKEY));

@devinrhode2
Copy link

@james4388 How is your example any different from the one from @beenotung?

@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Nov 5, 2020
@dead-claudia
Copy link

FYI: #26797

(Just found it - I'm not actually part of the TS team.)

@monfera
Copy link

monfera commented Dec 29, 2020

I agree that if this is safe:

const keepAs = <
  R extends Record<string, unknown>,
  From extends keyof R,
  M extends Record<string, From>,
  To extends keyof M,
  S extends { [P in To]: R[M[P]] }
>(
  a: R[],
  m: M,
): S[] => a.map((r) => Object.assign({}, ...Object.entries(m).map(([k, v]) => ({ [k]: r[v] }))));

then the analogous

const keepAs2 = <
  R extends Record<PropertyKey, unknown>,
  From extends keyof R,
  M extends Record<PropertyKey, From>,
  To extends keyof M,
  S extends { [P in To]: R[M[P]] }
>(
  a: R[],
  m: M,
): S[] => a.map((r) => Object.assign({}, ...Reflect.ownKeys(m).map((k) => ({ [k]: r[m[k]] }))));

is also safe

@Gnucki
Copy link

Gnucki commented Jan 29, 2021

My poor workaround for some cases :

const bar: Record<any, string> = {};
const FOO = Symbol('foo');

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const aFOO = FOO as any;

bar[aFOO] = 'sad';

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Jan 30, 2021

@Gnucki It’s probably better to do, since the as any type assertion gets removed during compilation:

const bar: Record<any, string> = {};
const FOO = Symbol('foo');

bar[
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	FOO as any
] = 'sad';

which compiles to:

const bar = {};
const FOO = Symbol('foo');

bar[FOO] = 'sad';

Whereas your code compiles to:

const bar = {};
const FOO = Symbol('foo');

const aFoo = FOO as any;

bar[aFOO] = 'sad';

which causes the local DeclarativeEnvironmentRecord to have two const bindings pointing to the same Symbol value.

@Repugraf
Copy link

Repugraf commented Mar 3, 2021

Is there any explanation of this from typescript maintainers team?
I can't see any reason why this is not handled.
This is very useful when creating 3 party libraries.
Without this fix we'll have to use @ts-ignore comments all over the place

@riggs
Copy link

riggs commented Mar 4, 2021

I'll reiterate my suggestion to make a TS language-level distinction between user-instantiated symbols and 'well-known' JS symbols.

Quoting myself from earlier:

Well-known symbols are just sentinels that happen to be implemented using Symbol. Unless the goal is object serialization or introspection, code probably should treat these sentinels different from other symbols, because they have special meaning to the language. Removing them from the symbol type will likely make (most) code using 'generic' symbols more safe.

@ljharb
Copy link
Contributor

ljharb commented Mar 4, 2021

@riggs that would not fit a number of use cases; it's absolutely critical that everything for which typeof x === 'symbol' is in the symbol type, otherwise APIs can't be typed properly.

@riggs
Copy link

riggs commented Mar 14, 2021

Given that TypeScript is a superset of JS created for the explicit purpose of type safety, create two new types, LanguageSymbol and Sentinel:

  • LanguageSymbol is the union of the 'well-known' static properties on Symbol.
  • Sentinel is every Symbol that isn't a LanguageSymbol.

Symbol remains untouched, but maybe gets a flag warning about direct usage in a future version.

Sentinels are, by definition, freed from the concerns relating to well-known symbols. Preserving this behavior at runtime does incur a small overhead, but would likely only occur when doing introspection, at which point extra care is likely warranted. I suspect most code doing runtime type checks for symbols are expecting to not be dealing with a LanguageSymbol in the first place.

@monfera
Copy link

monfera commented Mar 16, 2021

Nit: TypeScript is not a superset of JS, nor it is a programming language; it specifies little in the way of behaviors. One can't "develop code in TS 4.2". One codes in eg. ES2015, with TS 4.2 specified type annotation syntax and ES2015 library types. Novice programmers can't learn to "code in TS". They learn to code in JS, with type annotations. No TS spec says anything about what [].map() does, and it can't, as the runtime is JS and what the [].map() does depends on the ES version, not on the TS version. The coder can't put TS expressions in the Dev Console and there's no TS REPL. One can't implement a cleanroom TypeScript interpreter or compiler. Specifying the TS version and not specifying the ES version leads to unclear runtime semantics.

TypeScript has no up to date specification, let alone standard, and Microsoft's own 2016 writeup hasn't been vetted by standards bodies and isn't nearly anything like the EcmaScript specification from which one can actually implement a complying and useful realization. This document claiming TS is "a superset of EcmaScript 2015" doesn't make it so. There has been a stated disinterest in a specification for the last 5 years. Everything is a tradeoff, and this can be a legit tradeoff, and something defined by its own implementation, rather than a spec, doesn't make it not a language, though the existence of an ES-like spec would help establish TS as a language.

To quote from the 2016 document, "[besides some class and module notations] TypeScript also provides to JavaScript programmers a system of optional type annotations. These type annotations are like the JSDoc comments". It's an affordance for JavaScript programmers.

TS is more of a type annotation overlay to assist code linting, which also happens to add minor shorthands to the JS syntax via transpilation, and in practice, remove significantly from JS capabilities too (because it makes certain patterns very hard or impossible to properly type annotate; eg. it allows extra properties). TS does not live independently of JS, it has no language spec. And sure, there's an associated toolchain with source code transformation (mostly, removes the type notations), linter, IDE plugins etc.

So it's best to think of TS

  • a linter friendly type notation syntax, with
  • rather partial type libraries for constructs of ES versions, and
  • a toolchain consisting of linters, pessimizing transpilers (slow, unneeded polyfills) etc., plus
  • some macros for localized JS syntax augmentation (eg. in class),
  • which altogether constrain the coding style to the subset of JS patterns that are somewhat conducive to the otherwise not well specified, and not particularly consistent so-called structural typing concepts
  • associated with TypeScript versions, which bundle specifics of the above things, but still isn't a superset of JS

@DanielRosenwasser
Copy link
Member

We're working on an implementation, but the conversation here isn't useful nor is it contributing anything to the implementation.

@microsoft microsoft locked as off-topic and limited conversation to collaborators Mar 16, 2021
@ahejlsberg
Copy link
Member

Implemented in #44512 which is now in the main branch.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Fix Available A PR has been opened for this issue Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet