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.
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