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 extends keyof 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:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
which means for every K
, the object described by this type must have a property P
of same type as the property K
in T
. Your example playground code was:
const person: Pick<PersonProps, keyof PersonProps> = { age: 123 };
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:
const person: Pick<PersonProps, "age"> = { age: 123 };
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}
and Pick<PersonProps, "age">
are compatible, you might as well keep the type impicit:
const person = { age: 123 };
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With