Intro to Generics with Type-Level-TypeScript

The TypeScript beginner’s handbook I wish I had when I was a beginner.

Have you ever come across a resource that’s so good, you think to yourself, gee whilikers,

had only that I had had this resource whilst I were but a wee sapling, mightier still such an oak may haft sprang fortht?


This is what I though to myself when I came across Type-Level Typescript.

Simple, explained in terms that make sense, and great code examples to boot.

What really makes this site special, though, is the interactive code tests at the end of each section.

Below I will give my take on Generics, and show a few code examples with explanations.

What da heck is Generics

Functions in JavaScript (and TypeScript) have signatures.

A function’s signature is like a formula.

It’s probably best thought of as a way to describe how a function’s inputs and outputs relate to each other.

One caveat here being that in JavaScript, we don’t define outputs.

For example, in something like the below:

Function Signature Javascript

the function signature is simply the function’s name (addByTwo) and argument (num).

Unfortunately, simply looking at the function name and inputs doesn’t give us a whole lot of rich information about what the function does.

Especially compared with other languages where we explicitly define inputs and outputs.

One of the ways TypeScript tries to assuage this issue is by including types, and for that matter, return types.

Our earlier function now becomes something like this:

Function Signature Typescript

Now by simply glancing at this function’s signature we get a whole hecka lot more information.

We can now see that it must take a number as an input and return a number as an output.

Without explicitly needing to look at this function’s definition (the code inside the curly braces),

we can more or less infer what its purpose is, and how the pieces relate to one another.

Generics take this relationship business to the next level.

Let’s say we have a function that takes in a string ’Alright’

And returns an array where that string has been duplicated 3 times:

Function which takes a string and returns an array of that string, repeated 3 times

Ok, simple enough to create TypeScript types for this. A function that takes in a string and returns an array of strings.

The next day your boss comes to you with a request.

Your boss says to you, this is no good {firstName} {lastName}!

The shareholders are not happy! Not happy at all!

Now you have to change this function so its input is not necessarily a string.

It could be a string, it could be a number, it doesn’t really matter, but what is important,

is that whatever thing it takes in, it must return an array of three of those things.


This is the exact scenario Generics are built for;

Variable inputs/outputs where the type of the input and the type of the output are somehow related.

Function which takes a thing and returns an array of that thing, repeated 3 times

With a pattern like this, we get FAR more information about the purpose of the function simply by looking at its signature,

and it is now flexible enough to satisfy your boss and shareholders.

Lettuce take a look at a few examples from Type-Level Typescript:


function identity(a: TODO): TODO {
  return a;
}

identity('Derek');
// expected result -> 'Derek'

What can we infer here? We know that this function will need to take in a value (parameter), and return a value which is of the same type as the parameter.

This is a great example of a function that can make use of a single generic value:


function identity<T>(a: T): T {
  return a;
}

identity('Derek');
// expected result -> 'Derek'

The first T is best thought of as a representation that this function will make use of a single generic value.

The second T, used as a type for the parameter ’a’, shows that the value the function takes in may be of any type.

The third T, used as a return type, shows us that the value this function returns must be of the same type as the parameter it took in.

Another one, now using a function signature which requires 2 generic values:


function map(
  array: TODO[],
  fn: (value: TODO) => TODO
): TODO[] {
  return array.map(fn);
}

map(
  ['peas', 'carrots'],
  (str) => str.length
);
// expected result -> [4, 7]

What is the relationship between the inputs and the outputs here?

We know the function takes an array of things. Here they are strings, but really their type could be anything.

It also takes in a (callback) function which will run over each value of the array.

So we know that the value this callback function takes in as a parameter must be of the same type as each element in the array. We can call this value T.

The callback function returns a thing. Its type could be anything, and is completely independent of the elements of the array, as well as independent of the parameter the callback function takes in.

We can call this 2nd variable value U.

We would then write this function like:


function map<T,U>(
  array: T[],
  fn: (value: T) => U
): U[] {
  return array.map(fn);
}

map(['peas', 'carrots'],
(str) => str.length);
// expected result -> [4, 7]

Now one which uses 3 generics:


function pipe2(
  x: TODO,
  f1: (value: TODO) => TODO,
  f2: (value: TODO) => TODO
): TODO {
  return f2(f1(x));
}

pipe(
  { name: 'Alice' },
  user => user.name,
  name => name.length > 5,
);
// expected result => false

We have a function that takes in a value, then passes that value through a callback function and transforms it into a different value.

This new value may or may not be the same type as the original value.

We then take the result of the first callback function and pass it through a second callback function.

The result of the 2nd callback function becomes the return value.

Its value may or may not be the same type as the value passed into the 2nd callback function.

In total, then, we need 3 generic values:


function pipe2<T,U,V>(
  x: T,
  f1: (value: T) => U,
  f2: (value: U) => V
): V {
  return f2(f1(x));
}

pipe(
  { name: 'Alice' },
  user => user.name,
  name => name.length > 5,
);
// expected result -> false