"Dynamic" type when filtering object keys

Viewed 125

Let's say that I have the following payload:

{a: 1, b: 2, c: 3}

And I want to have a function that, given an object, picks some keys and returns a type accordingly.

function pick<T>(obj: any, ...keys: string[]): T {
  return (
    Object.fromEntries(
      keys
        .filter(key => key in obj)
        .map(key => [key, obj[key]])
    )
  ) as T;
}

const test = { a: 1, b: 2, c: 3 };

type Foo = {
  a: number,
  b: number
}
const result = pick<Foo>(test, 'a', 'b');
console.log(result);

Is there a way to handle this better? I understand that using type coercion is pretty much shutting down Typescript safety-net.

For example, if I do this:

console.log(pick<Foo>(test, 'b'));

I am telling typescript that the function pick returns a Foo object, yet it doesn't have the key a defined.

The use case is that we have API calls that we:

  1. Have to type
  2. Only want to type the relevant keys for our use case
  3. Do it following TS best practices
1 Answers

If you're okay with the implementation of pick not typechecking, then you can at least make the types all line up. The super-handy Typescript type we're going to use for this is actually called Pick.

function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
  return (
    Object.fromEntries(
      keys
        .filter(key => key in obj)
        .map(key => [key, obj[key]])
    )
  ) as any;
}

The function implementation is the exact same, except that at the end we cast through any, which effectively tells Typescript to trust us. Pick<T, K> is exactly the type you want: it says "take the type T, subscript it with the type K, and see what we get". K is an arbitrary type that extends keyof T. Assuming you pass constant arguments to this function, you'll get a union of literals type like "a" | "b" for K.

I don't know of a way to get Object.fromEntries to typecheck, short of casting through any like we did above. In my experience, when you start messing with functions on the Object prototype, you have to coax Typescript along a little bit to get the right types on things.

Related