1

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.

4
  • Please clarify which of your questions is your primary question. Do you want to know why key is generalized to string? Or do you want to know how to implement pick without using type assertions? Commented Jul 22, 2022 at 13:47
  • Especially the second part. I would like to write this function without type assertions. Using any hides possible errors. Using as ... hides possible errors. I wonder if it is possible to implement pick without type assertions and if not why. To me it looks like all the information to the compiler should be available. Commented Jul 22, 2022 at 14:45
  • I'm pretty sure you're going to need at least one type assertion somewhere, or something else unsound, due to a longstanding bug/limitation; see ms/TS#13948. There's a bit of unsoundness in your code, since pick({a: 0, b: 1}, Math.random()<0.5?"a":"b") should presumably produce a value of type {a: number} | {b: number}, but your version claims to produce {a: number, b: number}. My suggestion here would be this. If that answers your question I can write up an answer; if not,what am I missing? Commented Jul 22, 2022 at 15:01
  • I am afraid that you answered my question, yes. Sad but true. Hope it will be fixed. Thanks for the bug link and the improvement. Your point also works with pick<Foo, "a" | "b">({a: 0, b: 1}, "a"), which might be a little bit simpler to understand. Commented Jul 22, 2022 at 15:27

2 Answers 2

2

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

Sign up to request clarification or add additional context in comments.

Comments

0

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

1 Comment

I can replace line 3 with ret["nonexistingkey"] = obj[key] and the compiler would not raise an error. The complexity of the function is minimal and the error is unlikely to happen. I wonder if it is possible to implement pick without assuming that my types are correct. It feels like an easy problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.