Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can TypeScript only Sometimes Index an Object by a String

Why can TypeScript index a typed object by string when that string is a constant or a simple string variable, but it's unable to index a typed object by a string if that string is pulled out of an array

That is, consider the following code

class Foo {
    public bar: string = 'hello';

    public test() {
        // this works
        console.log(this['bar'])

        // this also works
        const index = 'bar';
        console.log(this[index])

        // in both cases above I have successfully used
        // a string as an index for my type Foo

        // However, this doesn't work
        const props:string[] = ['bar']
        for(const [key,value] of props.entries()) {
            console.log(value); // prints 'bar' to terminal/console
            console.log(this[value])
        }

        // Nor does this
        for(let i=0;i<props.length;i++) {
            console.log(this[props[i]])
        }

        // when looping over an array of string and trying to use the
        // string to index the object, I get the following error
        // why.ts:20:25 - error TS7053: Element implicitly has an 'any'
        // type because expression of type 'string' can't be used to
        // index type 'Foo'.
    }
}

const foo = new Foo;
foo.test()


class Foo {
    public bar: string = 'hello';

    public test() {
        // this works
        console.log(this['bar'])

        // this also works
        const index = 'bar';
        console.log(this[index])

        // in both cases above I have successfully used
        // a string as an index for my type Foo

        // However, this doesn't work
        const props:string[] = ['bar']
        for(const [key,value] of props.entries()) {
            console.log(value); // prints 'bar' to terminal/console
            console.log(this[value])
        }

        // Nor does this
        for(let i=0;i<props.length;i++) {
            console.log(this[props[i]])
        }

        // when looping over an array of string and trying to use the
        // string to index the object, I get the following error
        // why.ts:20:25 - error TS7053: Element implicitly has an 'any'
        // type because expression of type 'string' can't be used to
        // index type 'Foo'.
    }
}

const foo = new Foo;
foo.test()

Both of these work.

console.log(this['bar'])
//...
const index = 'bar';
console.log(this[index])    

TypeScript's able to index my object by a string.

However, the later examples where I'm looping over a string array

const props:string[] = ['bar']
for(const [key,value] of props.entries()) {
    console.log(value); // prints 'bar' to terminal/console
    console.log(this[value])
}

for(let i=0;i<props.length;i++) {
    console.log(this[props[i]])
}            

won't run/compile. I get the following error.

why.ts:42:17 - error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Foo'.
  No index signature with a parameter of type 'string' was found on type 'Foo'.

42     console.log(foo[value])

So this error message -- expression of type 'string' can't be used to index type 'Foo' seems to run counter to my first two examples.

So what's going on here? Help a poor dynamic language programmer understand what TypeScript is trying to tell me. Bonus points for a sample that actually allows me to iterate over an array of strings and use one as an object index.

like image 935
Alan Storm Avatar asked Jan 28 '20 16:01

Alan Storm


People also ask

Is string an object in TypeScript?

In TypeScript, the string is sequence of char values and also considered as an object. It is a type of primitive data type that is used to store text data.

Can you index into a string in JavaScript?

Characters in a string are indexed from left to right. The index of the first character is 0 , and the index of the last character—in a string called stringName is stringName. length - 1 . If the index you supply is out of this range, JavaScript returns an empty string.

Has an any type because expression of type string can't be used to index?

The error "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type" occurs when we use a string to index an object with specific keys. To solve the error, type the string as one of the object's keys.

Is everything an object in TypeScript?

Object: It describes the functionality, object helps in representing the non-primitive types that is everything except number, string, boolean, big int, symbol, undefined and null. In TypeScript Object(O uppercased) is different from object(o lowercased).


1 Answers

The answer is simple, if typescript can prove the access is safe, the indexing is allowed.

When you write this['bar'] typescript sees the string literal and it can trivially check that this has a property bar

When you write const index = 'bar'; you may think the type of index is string but it is actually not, the type of index is the string literal type 'bar', so typescript will know the only possible value in index is 'bar'. Since index can only hold bar, typescript can check that the access this[index] is valid by checking this has a property bar

When you write const props:string[] typescript will not make any other inferences about props it is an array of string. This means when you access this[prop] typescript needs to be sure this is indexable by any string, which since it does not have an index signature, it is not and thus the access throws an error. If you use as const to make ts infer literal types for the array instead of string and remove the explicit annotation, you will be able to perform the index access:

const props = ['bar'] as const
for(const [key,value] of props.entries()) {
    console.log(value); 
    console.log(this[value])//ok
}

for(let i=0;i<props.length;i++) {
    console.log(this[props[i]])
}

Playground Link

You can also use a type assertion if you are sure prop is a key of this:

const props = ['bar']
for(const [key,value] of props.entries()) {
    console.log(this[value as keyof this])
}

Or if you want to get really fancy you can use a custom type guard or custom type assertion, but that seems like overkill here.

like image 167
Titian Cernicova-Dragomir Avatar answered Sep 17 '22 16:09

Titian Cernicova-Dragomir