Iterating over object keys in TypeScript can be a nightmare. Here are all the solutions I know of.
Quick Explanation
Object.keys
doesn't work because
Object.keys
returns an array of strings, not a union of all the keys. This is by design and won't be changed.
ts
functionprintUser (user :User ) {Object .keys (user ).forEach ((key ) => {// Doesn't work!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'.7053Element 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'.console .log (]); user [key });}
keyof typeof
in the right spot makes it work:
ts
constuser = {name : "Daniel",age : 26,};constkeys =Object .keys (user );keys .forEach ((key ) => {
ts
functionisKey <T extends object>(x :T ,k :PropertyKey ):k is keyofT {returnk inx ;}keys .forEach ((key ) => {if (isKey (user ,key )) {console .log (user [key ]);
Longer Explanation
Object.keys
Here's the issue: using Object.keys doesn't seem to work as you expect. That's because it doesn't return the type you kind of need it to.
Instead of a type containing all the keys, it widens it to an array of strings.
ts
constuser = {name : "Daniel",age : 26,};constkeys =Object .keys (user );
This means you can't use the key to access the value on the object:
ts
constnameKey =keys [0];
There's a good reason that TypeScript returns an array of strings here. TypeScript object types are open-ended.
There are many situations where TS can't guarantee that the keys returned by
Object.keys
are actually on the object - so widening them to string is the only reasonable solution. Check out
this issue
for more details.
For...in loops
You'll also find this fails if you try to do a for...in loop. This is for the same reason - that key is inferred as a string, just like
Object.keys
.
ts
functionprintUser (user :User ) {for (constkey inuser ) {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'.7053Element 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'.console .log (]); user [key }}
But for many cases, you'll feel confident that you know EXACTLY what shape that object is.
So, what do you do?
keyof typeof
Solution 1: Cast to
The first option is casting the keys to a more specific type using
keyof typeof
.
In the example below, we're casting the result of
Object.keys
to an array containing those keys.
ts
constuser = {name : "Daniel",age : 26,};constkeys =Object .keys (user ) asArray <keyof typeofuser >;keys .forEach ((key ) => {
We could also do it when we index into the object.
Here,
key
is still typed as a string - but at the moment we index into the user we cast it to
keyof typeof user
.
ts
constkeys =Object .keys (user );keys .forEach ((key ) => {
Using
as
in any form is usually unsafe, though - and this is no different.
ts
constuser = {name : "Daniel",age : 26,};constnonExistentKey = "id" as keyof typeofuser ;
as
is a rather powerful tool for this situation - as you can see, it lets us lie to TypeScript about the type of something.
Solution 2: Type Predicates
Let's look at some smarter, potentially safer solutions. How about a type predicate?
By using a
isKey
helper, we can check that the key is actually on the object before indexing into it.
We get TypeScript to infer properly by using the
is
syntax in the return type of
isKey
.
ts
functionisKey <T extends object>(x :T ,k :PropertyKey ):k is keyofT {returnk inx ;}keys .forEach ((key ) => {if (isKey (user ,key )) {console .log (user [key ]);
This awesome solution is taken from Stefan Baumgartner's great blog post on the topic.
Solution 3: Generic Functions
Let's look at a slightly stranger solution. Inside a generic function, using the
in
operator WILL narrow the type to the key.
I'm actually not sure why this works and the non-generic version doesn't.
ts
functionprintEachKey <T extends object>(obj :T ) {for (constkey inobj ) {console .log (obj [key ]);
Solution 4: Wrapping Object.keys in a function
Another solution is to wrap
Object.keys
in a function that returns the casted type.