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]
:
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"))
}
Comments
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
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!