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

Created on 1 Jun 2018  路  15Comments  路  Source: microsoft/TypeScript


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%20x%3A%20Record%3Ckeyof%20any%2C%20string%3E%20%3D%20%7B%7D%3B%0D%0Ax%5B'hello'%5D%20%3D%20'world'%3B%0D%0Aconst%20sym%3A%20symbol%20%3D%20Symbol('sym')%3B%0D%0Ax%5Bsym%5D%20%3D%20'symbol%20string'%

Working as Intended

Most helpful comment

@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 { }

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

All 15 comments

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

@weswigham both of your suggestions produce symbol-index.ts(4,3): error TS2538: Type 'unique symbol' cannot be used as an index type.

Playground Link: http://www.typescriptlang.org/play/#src=const%20x%3A%20Record%3Ckeyof%20any%2C%20string%3E%20%3D%20%7B%7D%3B%0D%0Ax%5B'hello'%5D%20%3D%20'world'%3B%0D%0Aconst%20sym%20%3D%20Symbol('sym')%3B%0D%0Ax%5Bsym%5D%20%3D%20'symbol%20string'%3B%0D%0A%0D%0Aconst%20sym2%3A%20unique%20symbol%20%3D%20Symbol('sym')%3B%0D%0Ax%5Bsym2%5D%20%3D%20'symbol%20string'%3B

Yeah.... We don't have symbol index signatures, and a Record<whatever, string> isn't going to have any specific symbol named properties. Suffice to say, right now there's no way in the typesystem to make a type which can have arbitrary symbol keys. There's an issue for it open... https://github.com/Microsoft/TypeScript/issues/1863 I think.

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

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

Got it. Thanks for the detailed explanation.

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

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

@simon-scherzinger You probably want something more along the lines of

const sbl = Symbol('content');

interface MyObject<T> {
    someProperty: string;
    another: number;
    [sbl]: T;
    [key: string]: any;
}

class MyObject<T> { 
    constructor(content: T) {
        this[sbl] = content;

        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); // `any`

@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 { }

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

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

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.

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

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siddjain picture siddjain  路  3Comments

weswigham picture weswigham  路  3Comments

blendsdk picture blendsdk  路  3Comments

uber5001 picture uber5001  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments