Typescript - expression of type 'string' can't be used to index type

Viewed 2468
// common.js

const boxNames = ['one', 'two'];

module.exports {
  boxNames,
}
const common = require("./common.js");
const boxNames = common.boxNames;
const messages = {
  'one': {
      id: 'some id',
      text: 'some text',
  },
  'two': {
    id: 'some id',
    text: 'some text',
  },
};
const disabled = boxNames.map((key: string) => messages[key]);

The above code produces the TypeScript error for the messages[key] statement:

TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ one: { id: string; text: string; }; two: { id: string; text: string; }; }'. No index signature with a parameter of type 'string' was found on type '{ one: { id: string; text: string; }; two: { id: string; text: string; }; }'.

I am declaring key as a string and the keys of messages are strings, so what's the problem?

(Worth noting here that messages is generated using the defineMessages method from the react-intl library.)

(I have seen a couple of answers on StackOverflow similar to this that refer to Object.keys, but am not sure how this relates to my TypeScript error.)

3 Answers

The keys of the object messages are not of type string, they are of type "one" | "two". In other words, they are string literals, which is a narrower type than simply string and cannot be indexed by the broader string type.

A simple way to get your example to compile is to turn the array of keys into a tuple:

const boxNames = ['one', 'two'] as const;

This effectively changes the type from string[] to ["one", "two"], and now when you map over them they will be of the correct type to index your object. As a side note, it is not necessary to explicitly set the type for key since this can be inferred from usage.

const disabled = boxNames.map((key) => messages[key]); // this is now fine

Another way you could type this is by assigning the keys of messages to the array:

const messages = {
  'one': {
      id: 'some id',
      text: 'some text',
  },
  'two': {
    id: 'some id',
    text: 'some text',
  },
};

const boxNames: (keyof typeof messages)[] = ['one', 'two'];

const disabled = boxNames.map((key) => messages[key]);

This is more dynamic than using tuples, and it will remain typesafe as you will not be allowed to include more strings in boxNames than exist as keys of messages. I imagine this would still work in the require scenario you are describing although I don't have an environment set up to test it right now.

I think the main takeaway here is that either boxNames needs to acquire a more specific type, or messages needs a less specific one. I'm assuming these keys must be an exact match, but perhaps your situation would actually be better suited by assigning messages a string indexed type instead - I think there's not enough context here to assume either way. That would mean dealing with the possbility in your code that you don't actually know if the array and object will match at run time.

Typescript does not know that the elements of boxNames are keys of messages.

There is a clear distinction between string and keyof.

There are several ways you can tell TS that a string is a key of an object.

Without testing it myself, you may be able to do this:

boxNames.map((key: keyof typeof messages) => messages[key])

You want to look up how to use the keyof keyword, it is necessary to index objects in strict mode.

When you create an object like the below without giving type, the inferred type of the object would be Record<'one' | 'two', string>.

const numberCounter = {
 one: 'one',
 two: 'two'
}

So, what we have to remember here the keys of the object would always be a union of literal types, and the values of the object would be a union of the type of value.

Here in the below example, keys are a union of 'one' and 'two', and values are only string type (union of string and string is a string.)

I used to type the object like below, so I never come across such a problem. Here I explicitly saying that my object keys are of string type.

const boxNames = ['one', 'two'] ;

interface Type {
  id: string;
  text: string;
}
const messages: Record<string, Type> = {
  one: {
      id: 'some id',
      text: 'some text',
  },
  two: {
    id: 'some id',
    text: 'some text',
  },
};
// key is already inferred as string
const disabled = boxNames.map(key => messages[key]);
console.log(disabled);

If you don't want to explicitly say that the object keys are string, then @lawrence-witt answer would be the best fit here.

There is one more way to make keys a union of "one" and "two".

const boxNames = ['one', 'two'];

const messages = {
  one: {
      id: 'some id',
      text: 'some text',
  },
  two: {
    id: 'some id',
    text: 'some text',
  },
};

type Key = keyof typeof messages;

// here we are doing type cast of string into union of `one` and `two`.
//boxNames key are of string type
const disabled = boxNames.map(key => messages[key as Key]);
console.log(disabled);
Related