Typescript provides some nice facilities for quick and easy error detection when writing React components. Writing React components in Typescript isn’t hard, but getting the most out of higher-order components in Typescript can be a bit tricky, especially when they need to be generic.

Typescript generics

Typescript generics are essentially a way to parameterize other types. A form or a data fetching component is a good use case for generics because both the form and the data fetching component don’t care about the types of the code inside of them. The data fetcher is parameterized over the type of data it returns, and the form is parameterized over the types of the fields in the form.

A form component

I’m going to demonstrate the use of generics to make a little form helper component. Using this component, Typescript will be able to check that the field names used are all spelled correctly and that they are not set to values of the wrong type. The full code can be found at this gist.

I’ll start with the component implementation itself, then move on to explain the types involved. Focus on understanding the Javascript implementation first before moving on to the types.

    class Form<FormFields> extends React.Component<
      Props<FormFields>,
      State<FormFields>
    > {
      
      constructor(props: Props<FormFields>) {
        super(props);
        this.state = { fields: props.initialValues };
      }
    
      public onChange: OnChangeHandler<FormFields> = (field, value) => {
        // Use your favorite method to merge objects
        this.setState({ fields: merge(this.state.fields, { [field]: value }) });
      };
    
      public render() {
        const { FormComponent } = this.props;
        const { fields } = this.state;
    
        return <FormComponent onChange={this.onChange} fields={fields} />;
      }
    }

The component takes a FormComponent and initialValues as props, and it stores form fields in its state. When rendering the passed component it passes an onChange handler and the current state of the form fields as props. That onChange handler takes the name of a field and the new value and simply updates the state of the component. Even without Typescript this is a pretty useful higher-order component.

The first thing to notice is that the Form component has a Typescript generic parameter named FormFields. This is the type of the fields stored in the form, and is passed to both the Props and State types as their own generic parameters.

Here’s the Typescript interfaces for State and Props.

    interface State<FormFields> {
      fields: FormFields;
    }
    
    interface Props<FormFields> {
      initialValues: FormFields;
      FormComponent: React.ComponentType<FormComponentProps<FormFields>>;
    }

The interface for State is small. It states to Typescript that the fields state property should be the same type as the FormFields that is passed to State. Props is a little larger. First it lists the type of initialValues which must be the same shape as the passed in FormFields (and it’s also the same shape as the fields in State). Next is the type for FormComponent. This is just a React component that takes these specific props. Note that FormComponentProps is not an interface. Recent versions of Typescript have allowed generics in type aliases, but with older versions this code would not have worked.

    type FormComponentProps<FormFields> = { fields: FormFields } & Handlers<
      FormFields
    >;

The component passed in is going to receive fields as a prop (the form fields), and it’s also going to receive every handler defined in Handlers. Handlers also gets the FormFields as a type generic.

    interface Handlers<FormFields> {
      onChange: OnChangeHandler<FormFields>;
    }

Nothing too interesting about Handlers. It just specifies a single onChange handler. The type for that is complicated so its extracted into a separate declaration.

type OnChangeHandler<FormFields> = <K extends keyof FormFields>(
  s: K,
  a: FormFields[K]
) => void;

This is really where a lot of the magic happens. This uses Typescript index types. From their documentation:

With index types, you can get the compiler to check code that uses dynamic property names.

Saying that <K extends keyof FormFields> tells Typescript that the function is generic over every key that exists in FormFields. This means that the only valid values that can be passed as the first parameter of onChange are values that are keys of FormFields, and the only valid values that can be passed as the second argument to onChange are values that match the type of the field passed as the first argument! Crucially the key is stored as the type K. It could be written differently.

type OnChangeHandler<FormFields> = (
  s: keyof FormFields,
  a: FormFields[keyof FormFields]
) => void;

This still provides spelling protection of the names of the fields, but it does not prevent mixing up the types of the fields because both arguments are generic over all keys, instead of both arguments being specified as a single key but the whole function being generic over all keys.

Using the form

To use the form component requires an extra line of boilerplate. This line exists to show Typescript what type to use for the generic FormFields type above. This form exists to select a favorite sports player.

class SportsForm extends Form<SportsFields> {};

This declares a new class named SportsForm that is simply a Form with some SportsFields. It’s necessary because there’s no syntax to fill in a generic type in a JSX expression. For boilerplate, it’s not too onerous. The SportsFields are pretty simple in this example, just a team name and a player number.

interface SportsFields {
  teamName: string;
  playerNumber: number;
};

And the final component should use the above declared SportsForm instead of Form directly. If I get any field names wrong or pass the wrong value to the onChange function, Typescript will warn me when it compiles the code.

<SportsForm
  initialValues={{ teamName: [], playerNumber: 0 }}
  FormComponent={({ fields: { teamName, playerNumber }, onChange }) => (
    <form>
      <input type='text' value={teamName} onChange={(e) => onChange("teamName", e.target.value)} />
      <input type='number' value={playerNumber} onChange={(e) => onChange("playerNumber", parseInt(e.target.value, 10))} />
    </form>
  )}
/>

One important point to note is that none of the props have types. They can all be inferred because of the line of boilerplate declared previously. In fact, putting types on them explicitly seems to confuse the Typescript compiler. All of that JSX can be written as if it wasn’t in Typescript at all which is pretty good for migrating away if necessary.

Wrap up

Generic components are a useful, albeit somewhat more complicated tool in the Typescript toolbox. They can provide an extra level of safety when writing components, tightening the feedback loop and finding errors quickly. Furthermore it can all be done with the complicated code encapsulated in the generic component itself and with minimal boilerplate leaking to the usage sites. I call that a win for Typescript.