添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
考研的手电筒  ·  Functions.Max 方法 ...·  7 小时前    · 
性感的小摩托  ·  中国经济周刊·  6 月前    · 
文质彬彬的野马  ·  ID.7 ...·  2 年前    · 

How to Iterate Over Object Keys in TypeScript

Matt Pocock
Matt Pocock

Iterating over object keys in TypeScript can be a nightmare. Here are all the solutions I know of.

Quick Explanation

  • Iterating using 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
    function printUser(user: User) {
    Object.keys(user).forEach((key) => {
    // Doesn't work!
    console.log(user[key]);
    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'.
    });
    }
  • Casting to keyof typeof in the right spot makes it work:
  • ts
    const user = {
    name: "Daniel",
    age: 26,
    };
     
    const keys = Object.keys(user);
     
    keys.forEach((key) => {
    (parameter) key: string
    console.log(user[key as keyof typeof user]);
    });
  • A custom type predicate can also work by narrowing the type inline.
  • ts
    function isKey <T extends object>(
    x: T,
    k: PropertyKey
    ): k is keyof T {
    return k in x;
    }
     
    keys.forEach((key) => {
    if (isKey(user, key)) {
    console.log(user[key]);
    (parameter) key: "name" | "age"
    }
    });

    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
    const user = {
    name: "Daniel",
    age: 26,
    };
     
    const keys = Object.keys(user);
    const keys: string[]

    This means you can't use the key to access the value on the object:

    ts
    const nameKey = keys[0];
    const nameKey: string
     
    user[nameKey];
    Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; age: number; }'. No index signature with a parameter of type 'string' was found on type '{ name: string; age: number; }'.7053Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; age: number; }'. No index signature with a parameter of type 'string' was found on type '{ name: string; age: number; }'.

    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
    function printUser(user: User) {
    for (const key in user) {
    console.log(user[key]);
    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'.
    }
    }

    But for many cases, you'll feel confident that you know EXACTLY what shape that object is.

    So, what do you do?

    Solution 1: Cast to keyof typeof

    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
    const user = {
    name: "Daniel",
    age: 26,
    };
     
    const keys = Object.keys(user) as Array<keyof typeof user>;
     
    keys.forEach((key) => {
    (parameter) key: "name" | "age"
    // No more error!
    console.log(user[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
    const keys = Object.keys(user);
     
    keys.forEach((key) => {
    (parameter) key: string
    console.log(user[key as keyof typeof user]);
    });

    Using as in any form is usually unsafe, though - and this is no different.

    ts
    const user = {
    name: "Daniel",
    age: 26,
    };
     
    const nonExistentKey = "id" as keyof typeof user;
    const nonExistentKey: "name" | "age"
     
    // No error!
    const value = user[nonExistentKey];

    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
    function isKey<T extends object>(
    x: T,
    k: PropertyKey
    ): k is keyof T {
    return k in x;
    }
     
    keys.forEach((key) => {
    if (isKey(user, key)) {
    console.log(user[key]);
    (parameter) key: "name" | "age"
    }
    });

    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
    function printEachKey<T extends object>(obj: T) {
    for (const key in obj) {
    console.log(obj[key]);
    const key: Extract<keyof T, string>
    }
    }
     
    // Each key gets printed!
    printEachKey({
    name: "Daniel",
    age: 26,
    });

    Solution 4: Wrapping Object.keys in a function

    Another solution is to wrap Object.keys in a function that returns the casted type.