I thought I understood the purpose of the new TS 2.1 Pick type, but then I saw how it was being used in the React type definitions and I don't understand:
declare class Component<S> {
setState<K extends keyof S>(state: Pick<S, K>, callback?: () => any): void;
state: Readonly<S>;
}
Which allows you to do this:
interface PersonProps {
name: string;
age: number;
}
class Person extends Component<{}, PersonProps> {
test() {
this.setState({ age: 123 });
}
}
My confusion here is that keyof S is { name, age } but I call setState() with only age -- why doesn't it complain about the missing name?
My first thought is that because Pick is an index type, it simply doesn't require all the keys to exist. Makes sense. But if I try to assign the type directly:
const ageState: Pick<PersonProps, keyof PersonProps> = { age: 123 };
It does complain about the missing name key:
Type '{ age: number; }' is not assignable to type 'Pick<PersonProps, "name" | "age">'.
Property 'name' is missing in type '{ age: number; }'.
I don't understand this. It seems all I did was fill in S with the type that S is already assigned to, and it went from allowing a sub-set of keys to requiring all keys. This is a big difference. Here it is in the Playground. Can anyone explain this behavior?
Short answer: if you really want an explicit type, you can use
Pick<PersonProps, "age">, but it's easier use implicit types instead.Long answer:
The key point is that the
Kis a generic type variable which extendskeyof T.The type
keyof PersonPropsis equal to the string union"name" | "age". The type"age"can be said to extend the type"name" | "age".Recall the definition of
Pickis:which means for every
K, the object described by this type must have a propertyPof same type as the propertyKinT. Your example playground code was:Unwrapping the generic type variables, we get:
Pick<T, K extends keyof T>,Pick<PersonProps, "name" | "age">,[P in "name" | "age"]: PersonProps[P], and finally{name: string, age: number}.This is, of course, incompatible with
{ age: 123 }. If you instead say:then, following the same logic, the type of
personwill properly be equivalent to{age: number}.Of course, TypeScript is calculating all of these types for you anyway—that's how you got the error. Since TypeScript already knows the types
{age: number}andPick<PersonProps, "age">are compatible, you might as well keep the type impicit: