8

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.

Alana Storm
  • 164,128
  • 91
  • 395
  • 599

1 Answers1

11

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.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 4
    Abandoning my answer so just adding documentation link as a comment: OP might want to read about [string literal types](http://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types) – jcalz Jan 28 '20 at 17:00
  • 1
    Thank you, this is exactly what I wanted to read. I suspect it was due to an inability to infer strings out of the array but hearing it from a clear expert always puts the mind at ease. Also, I didn't realize you could have actual constant arrays in TypeScript (vs. the variable being const but not its constants), and `keyof this` is sort of breaking my mind right now (in a good way). That you for helping me get back on my dynamic bulls—t :) – Alana Storm Jan 28 '20 at 21:09
  • @ALANSTORM FYI, `keyof` works for any type not just `this` – Titian Cernicova-Dragomir Jan 28 '20 at 22:04
  • Adding link to updated docs URL mentioned by @jcalz - https://www.typescriptlang.org/docs/handbook/2/types-from-types.html – schalkneethling Aug 24 '23 at 23:56