How to Use TypeScript Generics

A quick tutorial on one of TypeScript’s best features

Like the @@ syntax in Ruby, the <> of TypeScript Generics made me a bit nervous, so I just straight up avoided them for the longest time. But, it turns out I was a huge wuss, because generics are actually simple and super handy.

Generics loosen up rigid functions

All generics do is make functions more flexible. To demonstrate why we need this, let’s write a mirror function. All it will do is return what we put in:

const mirror1 = (thing: string) {

return thing;

} mirror1('hello');

> 'hello'

mirror1(12) // breaks

A good start, but a terrible mirror; it only reflects strings . We want it to return whatever we put in. This is where generics come into play.

Add type parameters

You can define a type as a parameter by putting it inside <> before the () :

const mirror2 = <MirrorType>(

thing: MirrorType

): MirrorType => {

return thing;

} mirror1<number>(12);

mirror1<string>('hi'); // non arrow version:

function mirror3<MirrorType>(

thing: MirrorType

): MirrorType {

return thing;

}

All we’re doing is telling our function what kind of type we want to use, and then we can reference it wherever. So, when we say <number> , that means it’s expecting our argument to be a number , and it will return a number type as well (although, in this case that return would be implicit). In the real world, it isn’t always this simple.

Get fancy now

Below we are using a generic for only one of our parameters:

const copyMachine = <CopyType>(

itemToCopy: CopyType,

numOfCopies: number,

) => {

return Array(numOfCopies)

.fill(itemToCopy);

} copyMachine<string>('hi', 3);

> ['hi', 'hi', 'hi']

// more ('hi', 3);> ['hi', 'hi', 'hi']// more array tricks in this link

You can also use multiple types:

interface MagicTypes<Type1, Type2> {

firstItem: Type1;

secondItem: Type2;

} const magicMirror = <Type1, Type2>(

arg1: Type1,

arg2: Type2,

): MagicTypes<Type1, Type2> => ({

firstItem: arg1,

secondItem: arg2,

});

ha-HA, pulled a sneaky on ya and threw in a generic interface and used it in our return value. That’s right, you can make interfaces generic as well. Generics let you treat types like just another kind of parameter, so they’re used in a lot of places, especially classes.

Do you need to pass in the types?

Surprisingly, you don’t. You always need to define them, however, if an argument is a basic type, TS can figure it out:

const mirror2 = <MirrorType>(

thing: MirrorType

): MirrorType => {

return thing;

}

// still works

mirror2('hi');

Essentially, TS says to itself: “No MirrorType was passed , but the given thing value is a string . That means MirrorType must be a string .” This reverse engineering of types comes in handy for basic values. As long as the argument types are basic, generic type parameters don’t need to be passed in. The only reason you’d actually use type arguments is if you have an interface (interfaces are a way to define the types inside objects, click that link for a quick crash course).

With and without interfaces

Here is how TS would type check the mirror function’s argument object:

mirror({

name: '',

mood: 'happy',

isHungry: 0,

}); // default TS types:

{

name: string;

mood: string;

isHungry: number;

}

That’s fine, but let’s say the interface is really:

type NumberBoolean = 0 | 1;

interface PersonType {

name: string;

mood: 'happy' | 'sad';

isHungry: NumberBoolean;

};

That's much more specific and gives us some added benefits. To use this interface, pass it in:

mirror<PersonType>({

name: '',

mood: 'happy',

isHungry: 0,

});

Now TypeScript knows that isHungry could never be a number like 34 , and mood has only 2 options.

A warning about JSX

Before you go off and define your own generics, be careful when creating generic arrow functions in .tsx files. While the TypeScript compiler can handle using generic functions, like useState<Interface> in React, it trips up when defining them. TS will suddenly think the type parameter is a jsx HTML tag. There are two options to get around this: don’t use an arrow functions, or extend {}.

Regular functions

If you don’t care about this binding, you could use a normal function:

const mirrorTSX = function<ThingType>(

thing: ThingType

) {

return thing;

} // works!

mirrorTSX(12);

Extending {}

Using the extends keyword tells the TS compiler that it’s not reading HTML:

const mirrorTSX = <ThingType extends {}>(

thing: ThingType

) => {

return thing;

} mirrorTSX('hi');

It’s not that common to define generics inside of .tsx files, but it’s pretty dang annoying when you get that error and don’t know why. I hope this saves you at least 25 minutes of Googling.

In Conclusion

We’ve barely skimmed the surface of this amazing feature, but it’s already more than enough to get you going. For a much deeper dive, check out this article. However, they say the best way to learn is to do, so get out there and be extremely generic. It’s certainly worked out for me.

happy coding everyone,

mike