Sometimes when you're trying to index on an object TypeScript will give you an error like this:
interface User {
name: string;
email: string;
password: string;
}
function printUser(user: User) {
const keys = ["name", "email", "password"];
for (const key of keys) {
console.log(user[key]) // Error! Element implicitly has 'any' type
// because expression of type 'string' can't be used to index type 'User'.
// No index signature with a parameter of type 'string' was found on type 'User'.
}
}
Let's figure out what's going on here.
Breaking it down
First, if we break out user[key]
into a variable and hover over the variable, we can see that it has the type any
:
Why is this happening? TypeScript tells us in the error. Let's look at the error again:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'User'.
No index signature with a parameter of type 'string' was found on type 'User'.(7053)
Starting with the first line, TypeScript tells us that user[key]
has the type any
. The reason why it's giving us this error is because noImplicitAny
is turned on.
Element implicitly has 'any' type
Why is this happening? TypeScript tells us that it's because a string
type can't be used to index on type User
.
because expression of type 'string' can't be used to index type 'User'.
Where's this string type coming from? Well, we're using the variable key
to index on user
, so let's look at its inferred type:
Ah, that makes more sense! TypeScript thinks that key
is of type string
. This comes from the type of keys
, which is string[]
.
The reason why TypeScript doesn't allow us to index on User
with type string
is because it doesn't have an 'index signature':
No index signature with a parameter of type 'string' was found on type 'User'.
An index signature is a thing you can add to interfaces when you only know the key and value types of an object. It tells TypeScript to allow you to index on an object with much broader types. For example, this:
interface UsersTable {
[id: string]: User;
}
would tell TypeScript to allow us to index on UsersTable
with anything that is of type string
.
This is the important part: if we don't have an index signature, TypeScript will have a stricter default. TypeScript will only allow us to index on an object if the variable we're using to index is assignable to the union of the keys of that object. So in the case of User
, TypeScript will only allow us to index with a variable assignable to the type "name" | "email" | "password"
. (You can read on type assignability here).
This means that we need key
to have a type of "name" | "email" | "password"
, or something narrower (a subset of that, for example "name"
or "email" | "password"
). How do we fix that?
Fixing the problem: Solution One
The first way we can fix this is pretty simple. Let's write out the type that TypeScript wants:
type UserKey = "name" | "email" | "password";
And let's tell it that keys
is an array of UserKey
s:
const keys: Array<UserKey> = ["name", "email", "password"];
This works! The error goes away and TypeScript is happy.
To make sure we understand why, let's go back over the inferred types of the variables. keys
is of type Array<UserKey>
(or UserKey[]
), which means key
is now of type UserKey
. UserKey
is shorthand for "name" | "email" | "password"
. Because "name" | "email" | "password"
is assignable to the union of the keys of User
, TypeScript lets us index on it. And now user[key]
doesn't have an any
type and is of type string
. Here's a TypeScript playground where you can experiment with this.
Yay, we fixed the error! But there's one small problem: what if we add keys to User
? What happens then? Well we'd have to update the UserKey
union with the new keys we added. There's also the potential to misspell a key or forget to add one, which TypeScript will scream at us for.
Fortunately, there's a way around this. The keyof
operator.
Solution two: The keyof
operator
Here's the documentation for the keyof
operator in the TypeScript handbook. The docs tell us that keyof
does this:
The
keyof
operator takes an object type and produces a string or numeric literal union of its keys.
So this does exactly the same thing as what TypeScript does under the hood for indexing! keyof
takes an interface / object and returns the union of all of the keys in it. So for our User
interface, it would return "name" | "email" | "password"
.
We can now use this to shorten our UserKey
definition:
type UserKey = keyof User
This works exactly the same as before, but now if we update User
, we don't have to touch UserKey
. Pretty great!
Top comments (0)