Search code examples
typescript

How to create function working like "in" operator and has key suggestion?


I try to create function working like "in" operator and has key suggestion in TypeScript and I don't know how to type predicate at return type Pick<T, K> is warning but code is working just fine.

const users = {
    "Alice": {
        "id": 1,
        "name": "Alice",
        "age": 30,
        "isActive": true
    },
    "Bob": {
        "id": 2,
        "name": "Bob",
        "birthYear": 1985,
        "hasSubscription": false
    },
    "Charlie": {
        "id": 3,
        "name": "Charlie",
        "isAdmin": false,
        "age": 34
    },
    "Diana": {
        "id": 4,
        "name": "Diana",
        "age": 28,
        "isVerified": true
    },
    "Eve": {
        "id": 5,
        "name": "Eve",
        "dob": "1992-05-15",
        "isEmployee": true
    }
}

type AllKeys<T> = T extends unknown ? keyof T : never


/** Pick<T, K> is warning*/
//@ts-ignore
function hasKey<T extends object, K extends AllKeys<T>>(obj: T, key: K): obj is Pick<T, K> {
  return key in obj
}



const userKey = "Alice" as keyof typeof users
const user = users[userKey]



if ("hasSubscription" in user) {
  user.hasSubscription
}
if (hasKey(user, "isVerified")) {
  user.isVerified
}

Playground

/** Pick<T, K> is warning
 fixing or better solution
*/
//@ts-ignore
function hasKey<T extends object, K extends AllKeys<T>>(obj: T, key: K): obj is Pick<T, K> {
  return key in obj
}

Solution

  • You want to use the Extract<T, U> utility type, not the Pick<T, K> utility type:

    function hasKey<T extends object, K extends AllKeys<T>>(
      obj: T, key: K
    ): obj is Extract<T, Record<K, any>> {
      return key in obj
    }
    

    The Pick<T, K> utility type is meant to act on a single object type T and results in a widened version which only has the K properties. So Pick<{a: string, b: number}, "a"> is {a: string}. That's not at all what you're looking to do.

    The Extract<T, U> type is meant to filter union type T to just those members assignable to U. So Extract<{a: string} | {b: number}, {a: any}> is {a: string}. That's a lot closer to what you're looking for.

    You want to filter unions in T to just those members which have a property at key K. That can be performed by comparing each member of T to Record<K, any>, using the Record<K, V> utility type.

    You can verify that it works as desired:

    if ("hasSubscription" in user) {
      user.hasSubscription // okay
    }
    if (hasKey(user, "isVerified")) {
      user.isVerified // okay
    }
    

    Playground link to code