Mental Model for TypeScript Generics

Matt Pocock
Matt Pocock

The TypeScript docs are mostly excellent. The TypeScript handbook is a shining example of what good docs look like. They're precise, well-maintained, well laid-out, and complete.

However, if there's one section could us a rewrite, it would be the section on Generics. This section assumes too much knowledge, teaches things in the wrong order, and misses out key information. The generics section of the TypeScript docs leaves you feeling that generics are complicated and mysterious.

Let's rectify that.

Type Helpers

Look at generics first on the type level. Imagine you have a type like this:

This is a literal string, matt, expressed in a type alias. This can't be altered later, so it behaves similarly to a const variable declaration in JavaScript:

Using generics, we can change Name to be a function. Add a type argument to it:

This adds a parameter to Name, meaning that whenever you use it, you'll need to pass in an argument:

This turns the type from a variable to a function. Now, its equivalent in JavaScript would be:

Since it's now a function, let's name it something more function-like:

LastName is currently unused, so as an example return an object from our type 'function':

The JavaScript equivalent would be:

You can then use our GetNameObject type function to create types, by passing it arguments:

In Total TypeScript's Type Transformations workshop this pattern is called 'type helpers'. Type helpers are really useful on the type level to create new types to help DRY up your code.

A common example is a Maybe type helper:

Generic functions

Now you know how to handle generics on the type level, what about generic functions?

Take a simple example, returnWhatIPassIn:

This function won't return whatever you pass in - it'll actually always return any on the type level. This is annoying, because it ruins your autocomplete on the thing you pass in:

Imagine you wanted to create this on the type level. You'd create a type helper called ReturnWhatIPassIn:

At the type level, you're taking in TInput - whatever it is - and returning it.

You can use a very similar syntax to annotate our function:

This is now a generic function. This means that when you call it, the type argument of TInput gets inferred from what you pass in:

Put this in simple terms.

A generic function is a type helper layered on top of a normal function.

This means that when you call the function, you're also passing a type to the 'type helper'.

Build up that function again, piece by piece to make it clearer starting from your JavaScript-only function again:

Get the structure of the 'type helper' set up first. You'll need to infer an TInput argument, and return that.

If you try and call this now, it'll infer what it returns as unknown:

You haven't told the type helper which arguments to infer from. You need it to infer the type of TInput from the input argument. Fix that like this:

So inside (input: TInput), you perform a mapping between the runtime argument - input - and the type you want it to infer - TInput.

Summary

This is the right way to think about generics - as a type helper laid over your function, with a mapping between them.

Generics get a lot more complex from here - multiple generics, generic constraints, generics hidden deep within other types - but this mental model stays the same.

Matt's signature

Share this article with your friends