Why does the new `Pick<T, K extends keyof T>` type allow subsets of `K` in React's `setState()`?
Asked Answered
S

1

8

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?

Sonorant answered 6/3, 2017 at 2:27 Comment(0)
B
11

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 };
Bizerte answered 6/3, 2017 at 4:17 Comment(5)
The type "age" can be said to extend the type "name" | "age". Ah, I wouldn't have guessed that, I would think that "name" | "age" extends "age", not the other way. Thanks for the explanation!Sonorant
Sorry to bump this again, but while I understand the behavior now (thanks!), I can't really rationalize how "age" is a super-type of "age" | "name" and not a sub-type, even though its clearly how the compiler sees it. It seems top be the inverse of how the seemingly equivalent interfaces {name, age} and {age} relate to each other. Can you explain this rationale?Sonorant
In a generic type constraint, perhaps it's best to read "extends" as "is a more specific type of." Consider type AB = "a" | "b"; and type ABC = "a" | "b" | "c";. If you have a variable const abc: ABC = "c"; then you could not assign that to a variable of type AB, but any valid value of type AB is a valid value of ABC. As AB is a more specific type of ABC, AB can be said to extend ABC.Bizerte
(Oops my last comment I used super-type/sub-type backwards.) Thanks for the explanation, I can see that meaning. I guess extends has unfamiliar meaning for union types for me, but I think it makes a bit more sense now. Thanks.Sonorant
Thanks for the answer. But how it is possible that React can use partial state in a parameter without declaring the properties in the state parameter as optional?Himation

© 2022 - 2024 — McMap. All rights reserved.