React + TypeScript Generics = Love

TypeScript Assertions, Generics, Type Narrowing, the whole bunch

A peculiar journey to a generic React component using TypeScript

How I learned to stop worrying and love the <T>

Ján Jakub Naništa
Webtips
Published in
8 min readJun 8, 2020

--

Don’t you just love the warm spring evenings when there’s time to stop and take a breath, feel the breeze and watch all the code // TODO DRY while the pipeline runs? Then as the last embers of the sprint burndown chart die out you look around at all the components for selecting a value from a list: <UserListWithSearchBar/>, <ProjectPicker/> and <PermissionSelector/> and think to yourself: there is a pattern to this!

And that’s how TypeScript generics finally meet React components in your codebase. But the journey can turn out to be much less simple and straightforward than it sounds.

In this article, I’ll try to show you my approach to the challenges I faced when creating reusable, generic React components using TypeScript. I would like to point out that:

  • I am going to be focusing on the TypeScript side of things rather than UI/UX
  • I am going to assume you are familiar with TypeScript and have used it to build a React component before
  • I am going to explore different approaches during our journey, not just show you the final solution

Preface

What we are going to be doing is building a component that allows you to find and select an item from a list. Our goal is a statically typed component that we can use all over our codebase without repeating the same thing over and over.

Our component will render a list of items (passed as a prop, let’s call it items). When the user selects or deselects an item our component will notify us by calling a prop (let’s call that one onChange). If we pass a value prop the component will mark the matching item in items as selected. Reminds you of the good old <select/> right? But what is interesting about this component is that as opposed to <select> it works with values of any type! Any type? ANY TYPE!

Act 1: The props

Let’s start by defining the props of our new component since they define the API we will use to interact with it:

Act 2: The component definition

Now that we have our props in place, let’s define the component itself. This might prove to be more difficult than expected — TypeScript will not allow us to make the component generic! Just look at the following example:

You will see that both attempts above will result in a compilation error — in the first case TypeScript does not allow us to add a generic type to const (it will say T could not be found), in the second case TypeScript thinks <T> is actually a JSX tag, not a type parameter. But we cannot stop now that we wrote our props!

So let’s travel back in time to when arrow functions were not a thing and use their ancient ancestor — a function keyword:

Great! But some of you might have already noticed that we lost something in the process. We defined a generic function Select that takes a single parameter, but nowhere did we specify that Select is a React component — if you now go ahead and return a Promise from Select, or specify invalid defaultProps TypeScript will not say a thing:

Now there are three types of developers in this world:

  • The optimist might shrug and say If the tests pass then dunno put an any wherever you need to make the pipeline green
  • The nitpicker will do anything to make sure the compiler will warn them before horrible things start happening
  • The pessimist will stare into the void thinking Oh god why have I not become a hairdresser when there was still time

And although all of them are valid reactions, it’s The nitpicker I want to focus on. Let’s see what they might try when making sure their code is not vulnerable to for example a hasty code review in a stressful situation.

The first approach we might try is just adding a return type to our Select:

But typing React.ReactElement | null feels a bit too fragile for my taste — it can easily get out of sync with React types. On top of that, we can still pass invalid defaultProps!

So it’s back to the drawing board. What if we create a helper type, AssertComponent, that will show an error if we don’t pass a valid React component as a parameter? Here’s what I mean:

No progress made! We don’t have any React.ReactElement | null in our code but we introduced two types, one of which is not even used. I think we’re in trouble here.

Unless…

OMG! The return type is checked, defaultProps work as expected, what more could we want? And all thanks to the quite recent TypeScript feature called assertion functions.

Assertion functions are very similar to type guards with one key difference — once called, they will affect the whole scope (the whole file in our case) and will give errors when returning a meaningless value or when setting invalid defaultProps on Select. Awwwww yissssss.

Act 3: The props, revisited

Now that we have our component defined and typed let’s look at the implementation. We’ll run into a problem almost immediately:

It looks like we must have forgotten a prop! Since we don’t know anything about the type T we need some help knowing how to get a unique identifier for such a value. We also need to know how to check which value is selected. So let’s adjust the props, let’s add a idFromValue prop that turns a value of type T into something that can be used as a key:

idFromValue will accept a value of type T and return its “id”, for example, value => value.id or value => value.type + value.subtype depending on what our T is. So let’s adjust our component:

But we are still only rendering a dummy div instead of anything useful. And again, not knowing anything about the type T we will need an extra hand, how else is Select supposed to know what to render?

We could copy the approach we used for idFromValue — we could add a prop, let’s call it labelFromValue, that would transform type T into something that React can render (in other words it would return a React.ReactNode). Then we could wrap this value in some presentational markup like so:

But this way our Select would always look the same! Always a checkbox and a label… I don’t know about you but that’s not what I call customizable, that’s just… sad. Plus I bet some of you already got triggered — yes, the bunch of random <div/> and <label/> tags we return from items.map should be moved to a separate component to keep things clean.

So let’s try taking that idea further. Instead of having Select render the HTML, we will move all the rendering into a whole new component — let’s call it SelectItem. This component will be generic too, we will call its props SelectItemProps<T>. We then pass such component to our Select using a new prop called itemComponent:

Looks good! Select became very small, easily testable, and we can customize its UI and UX by defining a SelectItem that fits our use-case.

There is a drawback though, one that might become obvious only as our codebase grows. Since SelectItem is now responsible for both knowing how to render T and for rendering the layout (those <div/>s and <label/>s), we would need to define a new SelectItem for every combination of T and UI! Oh noooo!

After a minute or two of intense head-scratching (and a dash of procrastination), a new idea appears — why not combine the labelFromValue approach with the itemComponent approach into something like this:

Perfect! We have separated the logic that turns the T into a React.ReactNode from the logic that displays checkboxes. That’s always good. We can now implement generic SelectItem to match our UI and UX needs, the create labelFromValue and idFromValue functions, pass them to Select and our work is done here.

So it looks like we accomplished what we were hoping for — we have a generic and customizable React component ready! Unless…

Act 4: The return of the product owner

Materialized into a real-life person, change requests creep into your lovely new component. Disguised as something easy, a ticket lands on your sprint board demanding Select to be able to select more than one item. On a technical refinement session, you agree that if the multiple prop is passed to Select then it will allow multiple selections.

The single select version of Select should stay the same, you pass an array of items, possibly one selected value and an onChange handler that is called with either undefined or a value from the items array.

The multiple select version should also accept an array of items, however now we can pass an array to our value prop and our onChange handler will be called with an array of values from items. The array will be empty if there is nothing selected.

What does that mean for our code? What types need to change? How could we accomplish this polymorphism? Could we still try becoming a hairdresser instead?

Enter type narrowing. It allows us to change the shape of the props depending on the value of the new multiple prop. All we need to do is create separate sets of props for all possible values of multiple — in our case true and false (but you can easily extend this approach to numbers, string literals, etc.).

In the example above we defined common props, BaseSelectProps, that are shared by both versions of Select. We then defined separate props for the single (SingleSelectProps) and multiple (MultipleSelectProps) versions. Then we defined SelectProps as a union of these.

An alternative approach is to exchange interfaces for types and make use of & type intersection operator, I am sure you can make the necessary adjustments if you prefer this approach.

Let’s now look at the changes we need to make in our component code. Since the single and multiple versions differ in how they receive their value and how they call onChange, we will need to change our logic to reflect this.

Love it! The product owner is happy, the compiler is happy, the QA is happy, life is good! Our Select is now generic, customizable, and flexible enough to support all our use-cases. Time for some well-deserved procrastination!

Conclusion

Now that we’re all done let’s look back at what we’ve learned:

  • How to define Generic React components so that we can make our code more reusable while keeping it strongly typed
  • How to use Type assertion functions to protect our code from cryptic errors and accidental breaking
  • How to utilize Type narrowing that allows our component to change behavior based on a prop

I hope these will help you create not just any but the most stable and sturdy codebase ever. If you want to see the complete code please check out the example repository on GitHub. And if you have any questions or remarks don’t hesitate to drop me a line in the comments section below!

--

--