TypeScript: Type 'symbol' cannot be used as an index type.

TypeScript Version: 3.0.0-dev.20180601

Code

const x: Record<keyof any, string> = {};
x['hello'] = 'world';
const sym: symbol = Symbol('sym');
x[sym] = 'symbol string';

Expected behavior: No errors. tsc --target es6 symbol-index.ts works with 2.8.4.

Actual behavior: symbol-index.ts(4,3): error TS2538: Type 'symbol' cannot be used as an index type. when running tsc --pretty false --target es6 symbol-index.ts with 2.9.1 or next.

Playground Link: http://www.typescriptlang.org/play/#src=const x%3A Record<keyof any%2C string> %3D {}%3B x[‘hello’] %3D ‘world’%3B const sym%3A symbol %3D Symbol(‘sym’)%3B x[sym] %3D ‘symbol string’%

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 28
  • Comments: 16 (4 by maintainers)

Commits related to this issue

Most upvoted comments

@weswigham @mhegazy Do we have any spec that describes that this behavior is intended? For now, the main point that I get from https://github.com/Microsoft/TypeScript/issues/24587#issuecomment-394014604 is that we forbid symbols as indexers because we have no symbol index signatures and for me it’s sounds like bug in compiler because:

  1. https://www.typescriptlang.org/docs/handbook/symbols.html says that Just like strings, symbols can be used as keys for object properties.
  2. https://developer.mozilla.org/uk/docs/Web/JavaScript/Reference/Global_Objects/Symbol says that A symbol value may be used as an identifier for object properties

According to refs above consider following use case. I am implementing DI container where i would like to provide library users ability to use Symbols as keys(to avoid components collision) for their components. In my code I have something like: interface ComponentsContainer { [key: PropertyKey]: Component; }

Could someone provide strong argumentation why it’s not a bug in compiler and this code shouldn’t work?

Can’t imagine how it could be marked as “work as intended”, since we have this “TypeScript is a typed superset of JavaScript” on main page. This is definitely a bug in a type system and should be fixed somehow.

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

@prshreshtha just write

const sym = Symbol('sym');

or if you must type annotate, write

const sym: unique symbol = Symbol('sym');

we forbid symbols as indexers because we have no symbol index signatures (this was a hole we patched in 2.9 - previously it would assume the string indexer type, which was very incorrect), however unique symbols - a type associated with exactly one symbol are fine, since they refer to exactly one property.

For the unbearable change, I gotta write a ugly line like:

const DEFAULT_LEVEL: string = Symbol("__default__") as any;

What a stupid shit…

Can’t believe that shit works.

Had to use as unknown as string though since linter dislikes straight-up 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' }

@mhegazy why you labling it “Working as Intended”? Please answer to the comment

Seems to be fixed by https://github.com/microsoft/TypeScript/pull/44512 !

Available in 4.4.0-dev.20210627

This issue seems mislabeled, but the main issue is #1863 and it’s still open (and stalled apparently).

Hmm. So this code working pre 2.9 is a bug?

Conceptually yes - the actual machinery to make it work was never in place - the symbols were just being mistaken for strings.

Oddly enough, the following works:

Yeah, that’s because square bracket accesses to {} are unchecked:

const sym: unique symbol = Symbol('sym');

const x: {} = {};
x[sym] = 42;
x["no"] = 12;

I don’t know how to feel about it, personally, but it’s been “a way out” of the type checker for awhile. Usages are an error under noImplicitAny, so it doesn’t come up too often.

But this doesn’t

Yeah, since the mapped type maps over string, it manufactures a string index signature in the resulting type. Since symbols can’t have index signatures, it quietly drops them (for now, at least).

Hmm. So this code working pre 2.9 is a bug?

Shouldn’t you always be able to index a property by PropertyKey (by definition)? Given that PropertyKey is string | number | symbol, I think it should follow that indexing by a symbol should always be possible.

Oddly enough, the following works:

let x: { [key in symbol]: string }; // typeof x is {}
x[Symbol()] = new Date();
x['hello'] = 'world';

But this doesn’t:

let x: { [key in symbol | string]: string }; // typeof x is { [key: string]: string } 
x[Symbol()] = 'value';
x['hello'] = 'world';

@weswigham

I have a question which is similar to this issue, I believe.

I author a library containig a similar class like MyObject you will find below. One main feature is that the content of MyObject can be different depending on the content. To enable users to write something like myObject.test I added the property [key: string]: any; to the interface.

With typescript 2.9.x this will result in Type 'unique symbol' cannot be used as an index type. error, but wenn I remove this line users will get Property 'test' does not exist on type 'MyObject'. when trying to access anything “dynamically added”.

Is there any way of doing this properly?

edit: It seems I found a workaround. If i declare the symbol const sbl: any = Symbol.for('content') typescript will accept it.

const sbl: any = Symbol.for('content'); // With `any` it will work

interface MyObject {
    someProperty: string;
    another: number;

    [key: string]: any;
}

class MyObject { 
    constructor(content: any) {
        this[sbl] = content; // Type 'unique symbol' cannot be used as an index type.

        Object.keys(content).forEach((key) => {
            // This is in fact more complex ;-)
            Object.defineProperty(this, key, {
                get: () => this[sbl][key],
                set: (val: any) => this[sbl][key] = val,
            });
        });
    }
}

export default MyObject;

const obj = new MyObject({ test: 'content' });
console.log(obj.test); // Property 'test' does not exist on type 'MyObject'.