DEV Community

Cover image for Using Typescript generics with your React components
Michael De Abreu
Michael De Abreu

Posted on

Using Typescript generics with your React components

Photo by Colin Cassidy on Unsplash

Recently I found here on dev.to one article talking about the usage of TS generics on React components. As you may notice, I've been working with TS since quite a while, and the first thing that I tried to work with in React was generics, because they are an amazing feature that allows us, component creators, to give some control over the types to end users, component consumers. Maybe we are both, maybe we are one, but no the other. In any way, a good typed component will always help.

But that article wasn't accurate, and it was uncompleted, so if someone else would read it, they will end up with misinformation. And I understand, we all are learning here, we all do mistakes, and that's not as important as it is for we to learn. So, I'm going to show you what are a generics, and how to use them in React. Also, why you can't use them in a good-readable-way right now, because, you can't sorry, more on that latter.

What are Generics?

Generic typing allows us to work with types that will be defined later. This allow us to reuse functions and components that otherwise would need specific typing to work, or not because we are using JS and at the end the typing is optional, we could use any, for example and it would work fine enough, but this is a TS article so I'm assuming you do want to use TS.

We can think of generic typing as variables for types. You can declare them, they will have a scope, and you can use them as you want.

How Typescript uses generics?

Typescript has been heavily influenced by C#, so it has some C-like structures and this is not an exception. Typescript defines and uses generics the same way C# does, with angle brakes (< and >). So, in order to use generics we need to declare them with angle brakes.

  • In functions
// Using named functions
function identity<Type>(arg: Type): Type {
  return arg;
}
Enter fullscreen mode Exit fullscreen mode
// Using inline typing and arrow function
const identity: <Input>(arg: Input) => Input = (arg) => arg;
Enter fullscreen mode Exit fullscreen mode
// Using arrow function with typing
const identity = <Input>(arg: Input): Input => arg;
Enter fullscreen mode Exit fullscreen mode
// Using an interface as a type
interface GenericIdentityFn {
  <Type>(arg: Type): Type;
}

const identity: GenericIdentityFn = (arg) => arg;
Enter fullscreen mode Exit fullscreen mode
  • In classes
class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}
Enter fullscreen mode Exit fullscreen mode

Generics could also use constrains, that allow us to ensure the type the user will use will have a minimum set of properties or methods. To define a constrain, we will use the keyword extends. You could also use some operators, like logic OR (|) and AND (&), and ternary conditional [condition] ? [return if true] : [else return] used in a feature called conditional typing.

So, using the last example we could write it like:

class GenericNumber<NumType extends number | bigint> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}
Enter fullscreen mode Exit fullscreen mode

and use it like:

const num = new GenericNumber<number>();
const big = new GenericNumber<bigint>();

num.zeroValue; // type number
big.zeroValue; // type bigint

num.zeroValue * big.zeroValue // Operator '*' cannot be applied to types 'number' and 'bigint'.(2365)
Enter fullscreen mode Exit fullscreen mode

Playground link

Typescript and JSX

Maybe you have notice it at this point, but JSX and TS both use angle brakes in their syntax, so how TS can understand when you want to use it to define an element and when to define a generic type?

It can't.

And that's why we have to follow certain rules in order to use JSX with TS. The first rule is to name the file with the extension tsx. This first rule is important, because it will tell TS that you will be using angle brakes to define JSX elements, most of the time. And because of that, there are a few things that we can not longer do with TS generics while we are working with JSX, most notably is that we can't type an arrow function like we did.

// Using arrow function with typing
                            // ^ Error: JSX element 'Input' has no corresponding closing tag.
const identityWithTypedFn = <Input>(arg: Input): Input => arg;
Enter fullscreen mode Exit fullscreen mode

Typescript Playground

But we could still use a named function to use generics, but we won't be able to type them as functional components. We can type arguments as props, and declare the return type, but it won't have any additional properties that function components do have, like
Component.defaultProps
. Now, thinking about this, it may not be such a bad thing, since we are using TS to cover to those, but you need to consider that we are not the ones that will be using this, and those will want to use a strongly typed functional components.

So, how can I use generics in React components?

Right not, there is no way to type functions declarations, there is a bug open for this (Typescript#22063), but if you want to create a type alias instead you can. I have another post that I recently write about this exact thing.

export interface Component extends VFC<ComponentProps<{}>> { <Data>(props: ComponentProps<Data>): ReturnType<FC> };
...
export const Component: Component = ({ data, keys }: ComponentProps<Record<any, any>>) => {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Because of how TS deals with type merging, it will take the value when needed, and the type when needed. But, this is a good example of high level typing. I suggest this to be used in the open bug because it doesn't have any functional trade-off, and someone said:

The trade off is complexity / readability. Although Typescript projects often already have their fair share of arcane expressions. Luckily you can hide most of them away so junior devs can just use the types without thinking about them too much (if the code base is in an okay state)

And I agree with that. If someone is learning to use generics, the last thing they will came up, if they came up with this at all, would be something like that. I just recently find about this, and if I would see this a couple years ago, most likely I wouldn't understand it. It's hard to understand, we are dealing with merging types, overloads of function signatures, and types and value merging, those are not concepts we want to learn when we are starting with something.

To good thing is, you don't have to. If you don't understand something, is best that you don't use it, if you don't have someone that can explain to some else what is going on when they ask. And if you do understand it, you probably will deliver something that less experienced programmers will consume, and this things will help them.

That's all folks!

Thank you for reading, and I really hope this can help you to understand better how generics works on TS and why we can't use them as we may like.

If you have any questions, don't doubt about asking them, I'll try to answer.

Happy coding!

Oldest comments (0)