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
K
is a generic type variable which extendskeyof T
.The type
keyof PersonProps
is equal to the string union"name" | "age"
. The type"age"
can be said to extend the type"name" | "age"
.Recall the definition of
Pick
is:which means for every
K
, the object described by this type must have a propertyP
of same type as the propertyK
inT
. 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
person
will 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: