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](https://fatv8.com/3sw663_1rkwqlqkzqrun/5przyzaw/2qx38/2qx80/244853564629f5052fb5923f8d3a0c4c.png)
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")
}
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