Ever got this error:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ foo: string; bar: string; }'. No index signature with a parameter of type 'string' was found on type '{ foo: string; bar: string; }'.(7053)

Yeah, me too. What used to be so simple in JavaScript suddenly feels hard in TypeScript.

In JavaScript,


const greetings = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
  alert(greetings[answer] || "OK")
}

To see it in action, I put it into a CodePen.

Now, port that to TypeScript,


const greetings = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
  alert(greetings[answer] || "OK")
}

Same. Except it doesn't work.
You can view it here on the TypeScript playground

This is the error you get about greetings[answer]:

TypeScript playground with error

Full error:

Element implicitly has an 'any' type because the expression of type 'string' can't be used to index type '{ good: string; bad: string; }'. No index signature with a parameter of type 'string' was found on type '{ good: string; bad: string; }'.(7053)

The simplest way of saying is that that object greetings, does not have any keys that are type string. Instead, the object has keys that are exactly good and bad.

I'll be honest, I don't understand the exact details of why it works like this. What I do know is that I want the red squiggly lines to go away and for tsc to be happy.
But what makes sense, from TypeScript's point of view is that, at runtime the greetings object can change to be something else. E.g. greetings.bad = 123 and now greetings['bad'] would suddenly be a number. A wild west!

This works:


const greetings: Record<string, string> = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
  alert(greetings[answer] || "OK")
}

All it does is that it says that the greetings object is always a strings-to-string object.

See it in the TypeScript playground here

This does not work:


const greetings = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === 'string') {
  alert(greetings[answer as keyof greetings] || "OK")  // DOES NOT WORK
}

To be able to use as keyof greetings you need to do that on a type, not on the object. E.g.
This works, but feels more clumsy:


type Greetings = {
  good: string
  bad: string
}
const greetings: Greetings = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === 'string') {
  alert(greetings[answer as keyof Greetings] || "OK")
}

In conclusion

TypeScript is awesome because it forces you to be more aware of what you're doing. Just because something happen(ed) to work in JavaScript, when you first type it, doesn't mean it will work later.

Note, I still don't know (please enlighten me), what's the best practice between...


const greetings: Record<string, string> = {

...versus...


const greetings: {[key:string]: string} = {

The latter had the advantage that you can give it a name, e.g. "key".

UPDATE (July 1, 2024)

Incorporating Gregor's utility function from the comment below yields this:


function isKeyOfObject<T extends object>(
  key: string | number | symbol,
  obj: T,
): key is keyof T {
  return key in obj;
}

const stuff = {
  foo: "Foo",
  bar: "Bar"
}
const v = prompt("What are you?")
if (typeof v === 'string') {
  console.log("Hello " + (isKeyOfObject(v, stuff) ? stuff[v] : "stranger"))
}

TypeScript Playground demo

Comments

Gregor

Hey Peter!

That's one of the annoying things to dance around in every new project. I've started bringing this method into every new codebase:

```ts
export function isKeyOfObject<T extends object>(
  key: string | number | symbol,
  obj: T,
): key is keyof T {
  return key in obj;
}
```

It's not a perfect solution though, since objects are open, or non-restrictive, in TS, meaning an object could be passed in with more (runtime) keys than those defined on its (compile-time) type. In that case the type would be wrong, as it would indicate its one of the (compile-time) keys (which is the smaller set).

Now I do still like it better than `Record<string, string>` (afaik the other version has identical semantics), since it does not make the object so permissive as to indicate that any string key would have some string value.

All the best from your still-in-Berlin friend Gregor

Peter Bengtsson

I had seen something like that before but didn't grok the connection. I updated the blog post to demo your utility function suggestion fully. Feels a bit better, to be honest.
Thanks! ...as always!

Your email will never ever be published.

Related posts