Joseph
Joseph

Reputation: 4715

Typescript: Type '{}' is not assignable to type 'Pick<T, K>', how do I type javascript `pick` function correctly?

I want to type a function called pick, I know typescript has built-in Pick<T, K>, now I want to implement the actual source code usage part, but I'm stuck.

What this function do is to pick provided object's property based on given strings and return new object.

The usage is this:

const data = { name: 'Jason', age: 18, isMember: true };
const pickedData = pick(data, ['name', 'isMember']);

console.log(pickedData)
// => { name: 'Jason', isMember: true } 

My pick function's javascript implementation without typescript is this:

function pick(obj, properties) {
  let result = {}

  properties.forEach(property => {
    result[property] = obj[property];
  });

  return result;
}

Here is the typescript version, but it's not compiling:

function pick<T, K extends keyof T>(obj: T, properties: K[]): Pick<T, K> {
  let result: Pick<T, K> = {}; // Typescript yells: Type '{}' is not assignable to type 'Pick<T, K>'.

  properties.forEach(property => {
    result[property] = obj[property];
  });

  return result;
}

My VSCode screenshot:

enter image description here

But, there is one solution, I don't know if this is a good solution, that is to add as Pick<T, K>:

function pick<T, K extends keyof T>(obj: T, properties: K[]): Pick<T, K> {
  let result: Pick<T, K> = {} as Pick<T, K> ; // Typescript now satisfy with this.

  properties.forEach(property => {
    result[property] = obj[property];
  });

  return result;
}

However, when using this pick function, typescript doesn't show the picked object property, instead, it shows unnecessary information:

enter image description here

It shows me this:

// typescript shows me, not what I want.
const pickedData: Pick<{
    name: string;
    age: number;
    isMember: boolean;
}, "name" | "isMember">

Is there any way that typescript can show me this?

// This is what I want typescript to show me.
const pickedData: {
  name: string;
  isMember: boolean;
}

Upvotes: 2

Views: 3391

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249726

The first problem is that {} should not be assignable to Pick<T, K> since Pick<T, K> will probably have required properties. There are some instances where a type assertion is the only way to get things to work and this is one of those. Typescript does not know that you plan to assign those properties a bit later, you have that information and provide it to the compiler in the form of the type assertion, basically saying, relax, I know this isn't really Pick<T, K> but I will make it into that shortly.

The second part of you issue is not so much a functional one Pick<{ name: string; age: number; isMember: boolean;, "name" | "isMember"> is structurally equivalent to { name: string; isMember: boolean; }. Sometimes the language service expands these types, sometimes it does not. This is a recent issue describing this exact behavior.

You can force an expansion of the type with a more complicated type (an identity mapped type in an intersection with {}):

type Id<T extends object> = {} & { [P in keyof T]: T[P] }
function pick<T, K extends keyof T>(obj: T, properties: K[]): Id<Pick<T, K>> {
  let result: Pick<T, K> = {} as Pick<T, K> ; // Typescript now satisfy with this.

  properties.forEach(property => {
    result[property] = obj[property];
  });

  return result;
}

const data = { name: 'Jason', age: 18, isMember: true };
const pickedData = pick(data, ['name', 'isMember']);

console.log(pickedData)

Just as a warning, there are no guarantees Id will always be expanded, all I can say is that in the current version Id seems to always force the expansion of the mapped type.

Upvotes: 5

Related Questions