Typescript: Allow indexing with symbols

Created on 30 Jan 2015  路  93Comments  路  Source: microsoft/TypeScript

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
Moderate Fix Available Suggestion help wanted

Most helpful comment

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_.

All 93 comments

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.

Yeah still looking for this today, this is what I see in Webstorm:

screenshot 2017-10-08 21 37 17

That actually works

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):
typescript 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.:
typescript 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:

https://stackoverflow.com/questions/53404675/ts2538-type-unique-symbol-cannot-be-used-as-an-index-type

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

  • Tests: I haven't had time to look into how tests in TS are organized and structured.
  • Localization: The diagnostic message has only been updated for the English variant. Maybe the other languages get the old message still. I don't know. I could only provide a German translation anyway.

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

Almost 5 years(

Please, implement this feature, I can't writing code normally..

Can participants provide actual examples in their use-cases?

I don't understand the protocol example and why it's not possible today.

Here's an example of StringConvertible

const intoString = Symbol("intoString")

/**
 * Something that can be converted into a string.
 */
interface StringConvertible {
    [intoString](): string;
}

/**
 * Something that is adorable.
 */
class Dog implements StringConvertible {
    [intoString](): string {
        return "RUFF RUFF";
    }
}

/**
 * @see {https://twitter.com/drosenwasser/status/1102337805336768513}
 */
class FontDog implements StringConvertible {
    [intoString](): string {
        return "WOFF WOFF";
    }
}

console.log(new Dog()[intoString]())
console.log(new FontDog()[intoString]())

Here's an example of Mappable or Functor (lack of higher-order type constructors aside):

const map = Symbol("map")

interface Mappable<T> {
    [map]<U>(f: (x: T) => U): Mappable<U>
}

class MyCoolArray<T> extends Array<T> implements Mappable<T> {
    [map]<U>(f: (x: T) => U) {
        return this.map(f) as MyCoolArray<U>;
    }
}

@DanielRosenwasser it seems like you're assuming all objects have an interface or are a class instance or are known in advance; using your last example, I should be able to install map, say, onto any javascript object (or at least, an object whose type allows for any symbol to be added to it), which then makes it Mappable.

Installing a property (symbol or not) onto an object after the fact is part of a different feature request (often called "expando properties" or "expando types").

Lacking that, the type you'd need for a symbol index signature would provide very little as a TypeScript user, right? If I understand correctly, the type would need to be something like unknown or just any to be somewhat useful.

interface SymbolIndexable {
   [prop: symbol]: any; // ?
}

In the case of protocols, it's generally a function, but sure, it could be unknown.

What I need is the symbol (and bigint) equivalent of type O = { [k: string]: unknown }, so I can represent an actual JS object (something that can have any kind of key) with the type system. I can narrow that later as needed, but the base type for a JS object would be { [k: string | bigint | symbol | number]: unknown }, essentially.

Ah I think I see @DanielRosenwasser point. I currently have code with an interface like:

export interface Environment<T> {
    [Default](tag: string): Intrinsic<T>;
    [Text]?(text: string): string;
    [tag: string]: Intrinsic<T>;
    // TODO: allow symbol index parameters when typescript gets its shit together
    // [tag: symbol]: Intrinsic<T>;
}

where Intrinsic<T> is a function type, and I want to allow devs to define their own symbol properties on environments similar to strings, but insofar as you could add [Symbol.iterator], [Symbol.species] or custom symbol properties to any interface, the index signature with symbols would incorrectly restrict any objects implementing these properties.

So what you鈥檙e saying is you can鈥檛 make the value type of indexing by symbol any more specific than any? Could we somehow use the unique symbol vs symbol distinction to allow this? Like could we make the index signature a default for regular symbols and allow unique/well-known symbols to override the index type? Even if it weren鈥檛 typesafe, being able get/set properties by symbol indexes arbitrarily would be helpful.

The alternative would be having users extend the Environment interface themselves with their symbol properties, but this doesn鈥檛 provide any additional type safety insofar as users can type the Symbol as whatever on the object.

@DanielRosenwasser here a real example of my production code. A State reused in many places as a map and may accept key of atom (domained feature). Currently I need to add symbol support, but I get a lot of errors:


Anyway current behavior is incompatible with ES standard that is wrong.

One additional late-night thought I had regarding symbol types. Why isn鈥檛 this an error?

const foo = {
  [Symbol.iterator]: 1,
}

JS expects all Symbol.iterator properties to be a function which returns an iterator, and this object would break a lot of code if it was passed around in various places. If there were a way to globally define symbol properties for all objects, we could allow for specific symbol index signatures while also allowing global overrides. It would be typesafe, right?

I am also not understanding, why a use case would be needed here. This is an ES6 incompatibility, which should not exist in a language wrapping ES6.

In the past I posted my findings on how this could be fixed here in this thread and if this code is not lacking important checks or features I doubt it's more time consuming to integrate it into the codebase than continuing this discussion.

I just didn't do a pull request, because I do not know about Typescript's test framework or requirements and because I do not know whether changes in different files would be necessary to make this work in all cases.

So before continuing to invest time to read and write here, please check if adding the feature would be less time consuming. I doubt anyone would complain about it being in Typescript.

Apart from all that the general use case is if you want to map values onto arbitrary symbols. Or for compatibility with non-typed ES6 code.

Here's an example of a place I think this would be helpful: https://github.com/choojs/nanobus/pull/40/files. In practice, eventNames can be symbols or strings, so I'd like to be able to say

type EventsConfiguration = { [eventName: string | Symbol]: (...args: any[]) => void }

on the first line.

But I might be misunderstanding something about how I should be doing this.

Simple use-case can't be done without pain:

type Dict<T> = {
    [key in PropertyKey]: T;
};

function dict<T>() {
    return Object.create(null) as Dict<T>;
}

const has: <T>(dict: Dict<T>, key: PropertyKey) => boolean = Function.prototype.call.bind(Object.prototype.hasOwnProperty);

function forEach<T>(dict: Dict<T>, callbackfn: (value: T, key: string | symbol, dict: Dict<T>) => void, thisArg?: any) {
    for (const key in dict)
        if (has(dict, key))
            callbackfn.call(thisArg, dict[key], key, dict);
    const symbols = Object.getOwnPropertySymbols(dict);
    for (let i = 0; i < symbols.length; i++) {
        const sym = symbols[i];
        callbackfn.call(thisArg, dict[sym], sym, dict); // err
    }
}

const d = dict<boolean>();
const sym = Symbol('sym');
const bi = 9007199254740991n;

d[1] = true;
d['x'] = true;
d[sym] = false; // definitely PITA
d[bi] = false; // another PITA

forEach(d, (value, key) => console.log(key, value));

I am also not understanding, why a use case would be needed here.

@neonit there are PRs to address this, but my understanding is that there are subtle issues with how the feature interacts with the rest of the type system. Lacking solutions to that, the reason I ask for use-cases is because we can't just work/focus on every feature at once that we'd like to - so use-cases need to justify work being done which includes long-term maintenance of a feature.

It seems that in fact, most people's imagined use cases won't be solved as easily as they imagine (see @brainkim's response here https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574550587), or that they're solved equally well via symbol properties (https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574538121) or Maps (https://github.com/microsoft/TypeScript/issues/1863#issuecomment-572733050).

I think @Tyler-Murphy gave the best example here in that you can't write constraints, which can be very useful for something like a type-safe event emitter that supports symbols.

So before continuing to invest time to read and write here, please check if adding the feature would be less time consuming. I doubt anyone would complain about it being in Typescript.

This is always easier to say when you don't have to maintain the project! 馃槃 I understand that this is something useful for you, but I hope you respect that.

This is an ES6 incompatibility

There are plenty of constructs that TypeScript can't type easily because it would be infeasible. I'm not saying this is impossible, but I don't believe that this an appropriate way to frame this issue.

So it seems like the inability to add symbol keys as index signatures comes from the fact that there are global well-known Symbols which require their own typings, which symbol index types would inevitably clash with. As a solution, what if we had a global module/interface which represented all known symbols?

const Answerable = Symbol.for("Answerable");
declare global {
  interface KnownSymbols {
    [Answerable](): string  | number;
  }
}

interface MyObject {
  [name: symbol]: boolean;
}

const MySymbol = Symbol.for("MySymbol");
const obj: MyObject = {
  [MySymbol]: true,
};

obj[Answerable] = () => "42";

By declaring additional properties on the global KnownSymbols interface, you allow all objects to be indexed by that symbol and restrict the value of the property to undefined/your value type. This would immediately provide value by allowing typescript to provide typings for the well-known symbols provided by ES6. Adding a Symbol.iterator property to an object which is not a function which returns an iterator should clearly be an error, but it is not one currently in typescript. And it would make adding well-known symbol properties to already existing objects much easier.

This usage of a global module would also allow Symbols to be used as arbitrary keys as well, and therefore in index signatures. You would just give the global known symbol properties precedence over the local index signature type.

Would implementing this proposal allow index signature types to move forward?

Individual use-cases are irrelevant. If it's cromulent JavaScript, it needs to be expressible in TS definitions.

but my understanding is that there are subtle issues with how the feature interacts with the rest of the type system

More like "refactors how index signatures internally work entirely so is a scary big change and raises cromulet questions as to how index signatures are or should differ from mapped types that don't use the template variable" to be precise.

It mostly led to a discussion on how we fail to recognize closed types vs open types. In this context, a "closed" type would be a type with a finite set of keys whose values cannot be extended. The keys of a kind of exact type, if you will. Meanwhile an "open" type in this context is a type which, when subtyped, is open to having more keys added (which, under our current subtyping rules, sorta all types are mostly sometimes, except types with index signatures which very explicitly are). Index signatures imply making an open type, while mapped types are largely related as though they are operating over closed types. This _usually_ works well enough because most code, in practice, is written with a structure compatible with closed object types. This is why flow (which has explicit syntax for closed vs open object types) defaults to closed object types. This comes to a head with generic index keys; If I have a T extends string, as T is instantiated broader and broader types (from "a" to "a" | "b" to string), the object produced is more and more specialized, right up until we swap over from "a" | "b" | ... (every other possible string) to string itself. Once that happens, suddenly the type is very open, and while every property may potentially exist to access, it becomes legal to, eg, assign an empty object to it. That's what happens structurally, but when we relate the generics in mapped types, we ignore that - a string constraint on a generic mapped type key is essentially related as though it makes all possible keys exist. This logically follows from a simple variance-based view of the key type, but is only correct is the keys come from a _closed_ type (which, ofc, a type with an index signature is never actually closed!). So, if we want to be backwards compatible, we _can't_ treat {[x: T]: U} the same as {[_ in T]: U}, unless, ofc, we want to, since in the non-generic case {[_ in T]: U} becomes {[x: T]: U}, adjust how we handle the variance of mapped type keys to properly account for the open type "edge", which is an interesting change in its own right that could have ecosystem ramifications.

Pretty much: Because it brings mapped types and index signatures much closer together, it raised a bunch of questions on how we handle both of them that we don't have satisfactory or conclusive answers to yet.

Individual use-cases are irrelevant.

This is, politely, pure madness. How in tarnation do we know whether or not we're adding a feature with the behavior people want without use cases by which to judge that behavior?

We're not trying to be difficult here by asking these questions; we are literally trying to ensure that we implement the things people are asking for. It would be a true shame if we implemented something we thought was "indexing with symbols", only to have the very same people in this thread come back and say that we did it totally wrong because it didn't address their particular use cases.

You're asking us to fly blind. Please don't! Please tell us where you would like the plane to go!

My bad, I could've been clearer about what I meant; it seemed to me that people felt they had to justify their actual code use-cases, rather than their desire to describe it more accurately via TS

So if I understand it correctly, this is mainly about the following problem:

const sym = Symbol();
interface Foo
{
    [sym]: number;
    [s: symbol]: string; // just imagine this would be allowed
}

Now the Typescript compiler would see this as a conflict, because Foo[sym] has an ambivalent type. We have the same issue with strings already.

interface Foo
{
    ['str']: number; // <-- compiler error: not assignable to string index type 'string'
    [s: string]: string;
}

The way this is handled with string indices is that specific string indices are just not allowed, if there is a general specification for string keys and their type is incompatible.

I guess for symbols this would be an omnipresent issue, because ECMA2015 defines standard symbols like Symbol.iterator, which can be used on any object and thus should have a default typing. Which they oddly do not have apparently. At least the playground does not allow me to run the Symbol.iterator example from MDN.

Assuming it is planned to add predefined symbol typings it would always lead to a general [s: symbol]: SomeType definition to be invalid, because the predefined symbol indices already have incompatible types so there cannot exist a common general type or maybe it would need to be a function type, because most (/all?) predefined symbol keys are of function type.

A problem with the mix of general and specific index types is the type determination when the object is indexed with a value not known at compile time. Imagine my above example with the string indices would be valid, then the following would be possible:

const foo: Foo = {str: 42, a: 'one', b: 'two'};
const input: string = getUserInput();
const value = foo[input];

The same problem would apply to symbol keys. It is impossible to determine the exact type of value at compile-time. If the user inputs 'str', it would be number, otherwise it would be string (at least Typescript would expect it to be a string, whereas it likely may become undefined). Is this the reason we do not have this feature? One could workaround this by giving value a union type containing all possible types from the definition (in this case number | string).

@Neonit Well, that's not the issue that's halted progress on an implementation, but that's exactly one of the issues that I waws trying to point out - that depending on what you're trying to do, symbol indexers might not be the answer.

If this feature was implemented, ECMAScript's built-in symbols wouldn't necessarily ruin everything because not every type uses those symbols; but any type that does define a property with a well-known symbol (or any symbol that you yourself define) would likely be limited to a less-useful index signature for symbols.

That's really the the thing to keep in mind - the "I want to use this as a map" and the "I want to use symbols to implement protocols" use-cases are incompatible from a type-system perspective. So if you had anything like that in mind, then symbol index signatures might not help you, and you might be better-served via a explicit symbol properties or maps.

What about something like a UserSymbol type that's just symbol minus built-in symbols? The very nature of symbols ensures there won't ever be accidental collisions.

Edit: Thinking about this more, 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.

@RyanCavanaugh here's my flight plan.

I have a system in which I use symbols like this for properties.

const X = Symbol.for(":ns/name")

const txMap = {
  [X]: "fly away with me!"
}

transact(txMap) // what's the index signature here?

In this case I want the txMap to fit the type signature of transact. But to my knowledge I cannot express this today. In my case, transact is part of a library that doesn't know what to expect. I do something like this for properties.

// please forgive my tardiness but in essence this is how I'm typing "TxMap" for objects
type TxMapNs = { [ns: string]: TxMapLocal }
type TxMapLocal = { [name: string]: string | TxMapNs } // leaf or non leaf

I can generate the set of types that fit transact from schema and use that. For that I'd do something like this and rely on declaration merging.

interface TxMap = {
  [DB_IDENT]: symbol // leaf
  [DB_VALUE_TYPE]?: TxMap // not leaf
  [DB_CARDINALITY]?: TxMap
}

But it would be nice if I could at least fallback to an index signature for symbols, I only expect transact to be handed plain JavaScript objects, I also only use symbols from the global symbol registry in this case. I do not use private symbols.


I should add that this is a bit of a pain.

const x = Symbol.for(":x");
const y = Symbol.for(":x");

type X = { [x]: string };
type Y = { [y]: string };

const a: X = { [x]: "foo" };
const b: Y = { [x]: "foo" }; // not legal
const c: X = { [y]: "foo" }; // not legal
const d: Y = { [y]: "foo" };

It would be super awesome if TypeScript could understand that symbols created via the Symbol.for function actually are the same.


This is also super annoying.

function keyword(ns: string, name: string): unique symbol { // not possible, why?
  return Symbol.for(":" + ns + "/" + name)
}

const x: unique symbol = keyword("db", "id") // not possible, why?

type X = {
  [x]: string // not possible, why?
}

That little utility function let's me enforce a convention over my global symbol table. however, I cannot return a unique symbol, even if it is created via the Symbol.for function. Because of the way TypeScript it doing things, it is forcing me to forgo certain solutions. They just don't work. And I think that's sad.

I've come across another use case where symbol as a indexing value would be useful, when working with ES Proxies to create a factory function that wraps an object with a proxy.

Take this example:

let original = {
    foo: 'a',
    bar: 'b',
    baz: 1
};

function makeProxy<T extends Object>(source: T) {
    return new Proxy(source, {
        get: function (target, prop, receiver) {
            return target[prop];
        }
    });
}

let proxied = makeProxy(original);

To match up the ProxyConstructor type signature the generic argument must extend Object, but that then errors because the generic argument isn't keyed. So we can extend the type signature:

function makeProxy<T extends Object & { [key: string]: any}>(source: T) {

But now it'll raise an error because the 2nd argument (prop) of get on ProxyHandler is of type PropertyKey which so happens to be PropertyKey.

So I'm unsure how to do this with TypeScript due to the restrictions of this issue.

@aaronpowell What is the problem you're facing? I see it is behaving fine:

let original = {
    foo: 'a',
    bar: 'b',
    baz: 1
};

function makeProxy<T extends Object>(source: T) {
    return new Proxy(source, {
        get: function (target, prop, receiver) {
            return target[prop];
        }
    });
}

let proxied = makeProxy(original);

function assertString(s:string){}
function assertNumber(x:number){}

assertString(proxied.foo); // no problem as string
assertNumber(proxied.baz); // no problem as number
console.log(proxied.foobar); // fails as expected: error TS2339: Property 'foobar' does not exist on type '{ foo: string; bar: string; baz: number; }'.

tsconfig.json:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "node",
    "target": "es2015"
  }

package.json:

{
  "devDependencies": {
    "typescript": "~3.4.5"
  }
}

@beenotung I see an error in the playground:

image

@aaronpowell the error appear when you enable 'strict' flag in the 'compilerOptions' in tsconfig.json.

So under the current version of typescript compiler, you've to either turn off the strict mode or cast the target into any ...

Sure, but an any cast isn't really ideal and disabling strict mode is just loosening restrictions in the type safety.

Reading messages i imagine the next "solution" will probably be to "disable typescript".

We shouldn't have to search for stopgap solutions neither have to explain why we need it.

It's a standard feature of javascript so we need it in typescript.

@DanielRosenwasser my use case is similar to that of @aaronpowell - a seeming mismatch in the ProxyHandler interface and TypeScript's rules preventing me from properly typing proxy handler traps.

A boiled down example demonstrating the issue:

const getValue = (target: object, prop: PropertyKey) => target[prop]; // Error

As far as I can tell, it's impossible to craft any type for target that will avert the error yet allow only objects that can legitimately be accessed by PropertyKey.

I'm a TypeScript newbie so please forgive me if I'm missing something obvious.

Another use case: I'm trying to have a type {[tag: symbol]: SomeSpecificType} for callers to provide a map of tagged values of a specific type in a way that benefits from the compactness of object literal syntax (while still avoiding the name clash risks of using plain strings as tags).

Another use case: I'm trying to iterate all enumerable properties of an object, symbols and strings both. My current code looks something like this (names obscured):

type ContextKeyMap = Record<PropertyKey, ContextKeyValue>

function setFromObject(context: Context, object: ContextKeyMap) {
    for (const key in object) {
        if (hasOwn.call(object, key)) context.setKey(key, object[key])
    }

    for (const symbol of Object.getOwnPropertySymbols(object)) {
        if (propertyIsEnumerable.call(object, symbol)) {
            context.setKey(symbol, object[symbol as unknown as string])
        }
    }
}

I'd very strongly prefer to be able to just do this:

type ContextKeyMap = Record<PropertyKey, ContextKeyValue>

function setFromObject(context: Context, object: ContextKeyMap) {
    for (const key in object) {
        if (hasOwn.call(object, key)) context.setKey(key, object[key])
    }

    for (const symbol of Object.getOwnPropertySymbols(object)) {
        if (propertyIsEnumerable.call(object, symbol)) {
            context.setKey(symbol, object[symbol])
        }
    }
}

I have also issue with indexing with symbols. My code is as follow:

const cacheProp = Symbol.for('[memoize]')

function ensureCache<T extends any>(target: T, reset = false): { [key in keyof T]?: Map<any, any> } {
  if (reset || !target[cacheProp]) {
    Object.defineProperty(target, cacheProp, {
      value: Object.create(null),
      configurable: true,
    })
  }
  return target[cacheProp]
}

I followed the solution by @aaronpowell and somehow managed to workaround it

const cacheProp = Symbol.for('[memoize]') as any

function ensureCache<T extends Object & { [key: string]: any}>(target: T, reset = false): { [key in keyof T]?: Map<any, any> } {
  if (reset || !target[cacheProp]) {
    Object.defineProperty(target, cacheProp, {
      value: Object.create(null),
      configurable: true,
    })
  }

  return target[cacheProp]
}

Casting to any from symbol isn't so nice indeed.

Really appreciated for any other solutions.

@ahnpnl For that use case, you'd be better off using a WeakMap than symbols, and engines would optimize that better - it doesn't modify target's type map. You may still have to cast it, but your cast would live in the return value.

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

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

I'm not agree. These three lines are equal to each other:

Object.assign(obj, {theAnswer: 42});
Object.assign(obj, {'theAnswer': 42});
obj['theAnswer'] = 42;

@DanielRosenwasser
I have this use case, In the playground link, I also solved it by using maps, but take a look, its ugly.

const system = Symbol('system');
const SomeSytePlugin = Symbol('SomeSytePlugin')

/** I would prefer to have this working in TS */
interface Plugs {
    [key: symbol]: (...args: any) => unknown;
}
const plugins = {
    "user": {} as Plugs,
    [system]: {} as Plugs
}
plugins[system][SomeSytePlugin] = () => console.log('awsome')
plugins[system][SomeSytePlugin](); ....

Playground Link

Using symbols here rules out the possible accidental overwrite that happens when using strings. It makes the whole system more robust and easier to maintain.

If you have an alternative solution that works with TS and has the same readability in the code, I'm all ears.

Any official explain for this issue?

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

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

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

Five years and Symbol as index still not allowed

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

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

FYI: https://github.com/microsoft/TypeScript/pull/26797

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

Was this page helpful?
0 / 5 - 0 ratings