Home > Mobile >  How would one implement the pick function using TypeScript
How would one implement the pick function using TypeScript

Time:07-23

The TypeScript documentation hints on a pick function, which is only declared and not implemented. I tried to implement a simplified version the following way:

function pick<T, K extends keyof T>(obj: T, key: K): Pick<T, K> {
  return { [key]: obj[key] }
}

I get the following error: "TS2322: Type { [x: string]: T[K]; } is not assignable to type Pick." I am wondering why key is generalized to string even if it is declared to be keyof T. Primarily, how could one implement pick without using any or casting like as Pick<T, K>? I also want to clarify that I do not want to use Partial<T> as a return type. I want to return a "slice" of the original type that contains exactly one field chosen by the caller.

Note: I also tried the equivalent:

function pick<T, K extends keyof T>(obj: T, key: K): { [key in K]: T[K] } {
  return { [key]: obj[key] }
}

This (of course) gives essentially the same error. I am on TypeScript version 4.7.4.

CodePudding user response:

There is currently a limitation in TypeScript where a computed property key whose type is not a single string literal type is widened all the way to string. See microsoft/TypeScript#13948 for more information. So, for now, in order to use a computed property with a type narrower than string you will need to do something a little unsafe like use a type assertion.


One reason there hasn't already been a fix for this issue is that you can't simply say that {[k]: v} is of type Record<typeof k, typeof v>. If typeof k is a union type (or if it is a generic type, which might end up being specified as a union type), then Record<typeof k, typeof v> has all the keys from the union type, whereas the true type of {[k]: v} should have just one of those keys.

You would run into the same problem with your pick() implementation. The type of pick(obj, key) is not necessarily Pick<T, K>, precisely because K might be specified with a union type. This complication makes things a bit harder to deal with.

The "right" type for pick(obj, key) is to distribute Pick<T, K> across unions in K. You could either use a distributive conditional type like K extends keyof T ? Pick<T, K> : never or a distributive object type like {[P in K]-?: Pick<T, K>}[K].


For example, if you have the following,

interface Foo {
    a: number,
    b: number
}
const foo: Foo = { a: 0, b: 1 }
const someKey = Math.random() < 0.5 ? "a" : "b";
// const someKey: "a" | "b"

const result = pick(foo, someKey);

You don't want result to be of type Pick<Foo, "a" | "b">, which is just Foo. Instead, we need to define pick() to return one of the distributive types above, and we need to use a type assertion to do it:

function pick<T, K extends keyof T>(obj: T, key: K) {
    return { [key]: obj[key] } as K extends keyof T ? Pick<T, K> : never
}

And that results in the following result:

const result = pick(foo, someKey);
// const result: Pick<Foo, "a"> | Pick<Foo, "b">

So result is either a Pick<Foo, "a"> or a Pick<Foo, "b">, as desired.

Playground link to code

CodePudding user response:

You can try to build the object and then return it. Something similar to this

function pick<T, K extends keyof T>(obj: T, key: K): Pick<T, K> {
  let ret: any = {}  
  ret[key] = obj[key]

  return ret
}

You can see this working here

  • Related