Nov 17 2017

TypeScript Generics for React Components

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.

React TypeScript State & Props

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
Handler
s.
Handler
s 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.

Generic Functions

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.

Signaling Type Generics

TypeScript generics are an advanced feature of TypeScript that allows for parameterizing existing types. They are similar in concept to generics in Java. Syntactically, type generics are signalled by the use of angle brackets on function, class, and type declarations. Inside the angle brackets are the “type parameters”. It looks like this:

interface State<Param> {
\ data: Param;
}
const data = State<boolean>;

When a type uses a generic the type is referred to as a “higher-order type” (a type that takes types, just like a “higher-order component” is a component that takes components). The type parameter is kind of like a variable or a function argument. It serves as a placeholder for a specific (non-generic) type which will be provided when the type is used. A higher-order type that has all of its generics supplied is known as a “concrete” type. In the above example, State is the higher order type, Param is the generic, and State\<boolean> is the concrete type.

Parameterizing Types

Just like parameterizing functions with the DRY principle allows for creating smaller and less repetitive code by consolidating similar functionality, parameterizing types with generics allows for creating fewer and less repetitive types. This leads in general to clearer and more reusable code. Of course it is possible to go overboard, just like it is possible to go overboard with the DRY principle. Good uses of type generics are for extracting concrete type information from code that doesn’t actually care about what type something is. 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 only cares about about the logic for fetching the data and managing things like loading states, but doesn’t actually care about what the data is. It just passes it along. A from component just cares about logic around the state of its inputs, without caring about what those inputs return. That’s a little trickier to visualize, so I’ll dive into more detail in the next section.

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.

PS: Do you have a React project in the pipeline? Check out our React development services to learn more about our development engagements.

Daniel Wilson

Share: