Search code examples
reactjstypescripttypescript2.1

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


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?


Solution

  • 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 };