Building React Components with TypeScript Generics
Give your React Props a Generic Type
Strong typing; If you’re a React JavaScript developer who has turned to working in TypeScript, you’ve no doubt felt the cut of that double-edged sword.
On one hand, the strong typing that TypeScript provides often catches things that we might have otherwise overlooked and prevents disaster before it happens. Thank you TypeScript!
On the other hand, it also locks in our React props to a given set of types when we may not know what type the consumer will want to pass in. TypeScript, why are you tryin’ to control me?!
Imagine that we are tasked to build a reusable table component which will have a data
prop that is an array of the items which will make up the rows and columns.
We have no idea what the consumer will pass in so…. what the heck do we do now?
Don’t worry! The Great T
will solve everything!
Why bother? Why not just use “any”?
Well, that certainly is one way to live, but the question to ask yourself if you do this is, “Why am I bothering to use TypeScript at all?” In addition to that, if your component has any events that send data out, your consumer will have to recast your any
to their desired type.
“That’s not a big deal, bro. I think I’m going to stick with
any
.” — You (possibly)
It may not be a big deal if you have to do it only once or twice, but if you get in the habit of building components that way, it’ll add up to consumer frustration rather quickly. So let’s be customer focused!
The Scenario
I mentioned at the beginning that a table is a classic scenario for generics but, in an effort to keep this article more concise, let’s go with something else. It’s a little more contrived, but bear with me.
Let’s say that we are tasked to build an EmployeeInfo
component which will display an employee’s avatar, name, and job title.
We could easily create a component that could look like this:
<EmployeeInfo
firstName="John"
lastName="Smith"
title="Worker Guy"
image="http://somesite.com/john-smith.jpg" />
But let’s say there is one more requirement, when our component is clicked, we want all of John Smith’s available data to be returned. On top of that, this component will be used across teams with different contexts where the concept of an employee might go beyond this or be quite differently shaped.
Our goal will be to be able to pass in the full employee info into our component like this:
<EmployeeInfo
employee={info}
onClick={employee => console.log(employee)} />
The First Attempt
We’ll first start by defining what that info looks like:
Here is our first attempt at building our React component in TypeScript:
Notice that we’ve defined our props as an interface calledIProps
which defines the employee
prop as type IEmployeeInfo
.
Here is our app that is consuming this component:
…which will output this:
Everything is strongly typed and assuming everyone is using some form of IEmployeeInfo
. Just to prove that, we can extend IEmployeeInfo
into a different kind of employee:
And then we update our consuming app to use it instead of IEmployeeInfo
:
Which (no surprise) will output this:
This is because an IEmployee
“is” an IEmployeeInfo
. However, the downside is that our onClick
param is viewed as an IEmployeeInfo
by our code editor:
If the consumer doesn’t need any of the other details of IEmployee
, then this is a fine solution, but we can do better.
Let’s Go Full Generic Mode!
Now, what happens when a different team has a completely different shape for their employee data? This is where the power of the TypeScript generics come into play.
Let’s say this other team has a CustomEmployee
type that looks like this:
Every key is different from IEmployee
.
Let’s start by modifying our component’s IProps
interface to accept a generic type:
We’ve included the angle brackets with the letter T
to indicate an unknown type. By the way, we don’t have to use T
. We can use any designation we want, but T
is customary.
Notice that the type T
has been used for the employee
prop type on line #2 and for the employee
type in the parameter for the onClick
prop type defined on line #3. Whatever type we define forT
, it will be used in all of those places.
Next, we’re going to change part of our component’s function signature (ignore the added keys
prop for now):
We’ve also added T
to our component and we’ve given it a default of IEmployeeInfo
if the consumer doesn’t pass us anything for the type. We’re then handing T
over to the IProps
interface so the types of our props can be fully defined.
Please note, this is a functional component. If you have a class-based component, the syntax will obviously be different. We’ll get to what you need to do for that in a bit.
We’ve added a new prop called keys
which has the type Keys
that is defined like this:
You’ll notice that Keys
has the same keys as IEmployeeInfo
. Our plan is to use ourkeys
prop to map T
to an IEmployeeInfo
.
You’ll probably also note that I’ve added a convert
function at the bottom. This function (also generic) will convert a given object to an IEmployeeInfo
such that we can easily reference the members with type security throughout (see lines #20, 24, 27). This is how we’ll accomplish the mapping.
Let’s update our App.tsx:
We’re now using a CustomEmployee
type (lines #6–14) and we’ve defined our keys
map (lines #16–21).
Now, this is where the magic happens.
Take a look at how we’re consuming the component on line #25:
<EmployeeInfo<CustomEmployee>
We have passed in theCustomEmployee
type as our T
and that’s all there is to it. If we hover over the employee
parameter in our onClick
callback on line #28, you’ll see that it has correctly identified our type as CustomEmployee
:
If we run our project, we see:
BAM! We did it!
Awesome, but I’ve got a class-based component. What do I do?
Your changes will be similar:
The key difference to note is line #10. You are similarly defining T
on the component with a default of IEmployeeInfo
and passing it to IProps
.
To Sum Up
Functional Component Syntax
function EmployeeInfo<T = IEmployeeInfo>(props: IProps<T>): JSX.Element {/* Component code goes here. */}
Or if you prefer ES6 const syntax…
const EmployeeInfo = <T extends {} = IEmployeeInfo>(props: IProps<T>) : JSX.Element => {/* Component code goes here. */};
The one difference to note here is that we will have to use the extends = {}
syntax with ES6 in order for our IDE to know that this is intended to be a generic type and not the opening of a JSX tag.
In our case, we can omit the {} =
and simply extend IEmployeeInfo
, however, I left it in because you will need to include it when you have no idea what your type will be.
Personally, I find the extends {}
to feel a bit hacky which is why I generally favor the traditional, but you are free to use whichever you prefer.
Class-based Component Syntax
class EmployeeInfo<T = IEmployeeInfo> extends React.Component<IProps<T>> {/* Component code goes here */}
You’ll now be able to provide your consumers with strongly typed React components that are flexible enough to have the types that they want.