## Generics as Type Params in Typescript

2024-09-04

About a year ago, I wrote a post about an “Option” type in Typescript. In this post, I would like to revisit that and make a case for a new Typescript language feature: generics as type parameters (a.k.a higher-kinded types a.k.a. type classes).

Before we get into the proposal, I’d like to outline two types that will help me make my case: `Identity`

and `Maybe`

. Before that, let’s talk about a math-y term: functor.

### What is a functor?

As much as I would love to write about it, this post *isn’t* about category theory (I promise). That said, we *will* be mentioning the word “functor” (not to be confused with *function*). A functor must obey some mathematical laws and has implications in functional composition, but for our purposes, we only need to know one thing:

A functor is a container which can be mapped

For this post, let’s just focus on that. If an object has a `map`

function, then it’s a functor. We’ve already got one of those provided to us in JavaScript: `Array`

!

```
const strLen = (x: string) => x.length;
const fruit = Array.of("apple", "banana", "watermelon");
// type: string[]
// value: ["apple", banana", "watermelon"]
const fruitLen = Array.of("apple", "banana", "watermelon").map(strLen);
// type: number[]
// value: [5, 6, 10]
```

I am using Array.of here for illustrative purposes; it’s the same as defining the array with `= [...]`

.

### The `Identity`

functor

In my last post on this subject, I made a `Container`

type using pure functions. This time, I’m going to use a generic class and call it `Identity`

. Here’s the definition in full:

```
class Identity<T> {
_value: T;
constructor(x: T) {
this._value = x;
}
static of<A>(x: A): Identity<A> {
return new Identity(x);
}
map<B>(fn: (a: T) => B): Identity<B> {
return Identity.of(fn(this._value));
}
}
```

This doesn’t really do much. It just holds a value (`this._value`

) and let’s consumers run functions on it (`map`

). The type of `this._value`

is a generic `T`

- a container can hold any type. Here it is in action:

```
const taco = Identity.of("taco");
// type: Identity<string>
// value: { _value: "taco" }
const tacoLen = Identity.of("taco").map(strLen);
// type: Identity<number>
// value: { _value: 4 }
```

Notice that we can call `.map`

on an `Identity`

and it will update the type and run the function on the value stored within it. That’s all it does…just holds stuff and runs functions on the stuff.

### The `Maybe`

functor

The post I wrote last year focused on building up an `Option`

type. I worry that the implementation got a little complicated with pure functions, so I’m going to also use a class for this example as well. We’re calling it `Maybe`

this time around. Here it is in full:

```
class Maybe<T> {
_value: T;
constructor(x: T) {
this._value = x;
}
static of<A>(x: A): Maybe<A> {
return new Maybe(x);
}
map<B>(fn: (a: T) => B): Maybe<B> {
return this.isNothing() ? this : Maybe.of(fn(this._value));
}
isNothing() {
return this._value == null || this._value == undefined;
}
}
```

This is *almost* the same as `Identity`

. The difference is that we check if the value is `null`

or `undefined`

and, if it is, we don’t run `fn`

on the value. This helps us avoid error-handling in every function. Let’s see it in action:

```
const maybeRealLen = Maybe.of("Narwhal").map(strLen);
// type: Maybe<number>
// value: { _value: 7 }
const maybeNotRealLen = Maybe.of(null).map(strLen);
// type: Maybe<null>
// value: { _value: null }
```

Notice how we are able to call `.map(strLen)`

here and it *doesn’t* blow up when the value is `null`

. We also didn’t need to adjust our underlying `strLen`

function at all. That’s the power behind `Maybe`

.

There is another cool thing happening here: we’re using the same `strLen`

function in all these examples. Regardless of which functor we’re using, if the generic type (`T`

) is the same (`string`

) then we can use the same function in `map`

.

*Side note: Typescript isn’t 100% happy with this definition of Maybe, but I wanted to keep the typing complexity down.*

### Similarities

So we have three functors: `Array`

, `Identity`

, and `Maybe`

. One holds a set of values, one holds a single value, and one *might* hold a value. Since they’re all functors, they all have a `map`

method. They look *very* similar:

```
const fruitLen = Array.of("apple", "banana", "watermelon").map(strLen);
const tacoLen = Identity.of("taco").map(strLen);
const maybeRealLen = Maybe.of("Narwhal").map(strLen);
```

Removing what’s different will help us see the pattern here:

`<Functor>.of(<stuff>).map(<function>);`

And if you look closely at the `map`

method in each class…the method signature is *basically* the same:

```
class Functor<T> {
// ...
map<B>(fn: (a: T) => B): Funcor<B> {
// ...
}
}
```

Just swap out `Functor`

for `Identity`

or `Maybe`

(the built-in `Array`

works fairly similarly as well).

This is a *very* valuable property because we can write code that only works with container types (functors) and doesn’t need to care if it’s a `Maybe`

or some other type of functor (e.g. a `Task`

functor that runs an async operation). I know I’m kinda hand-waving here, but I promised you earlier I wouldn’t get into category theory. If you want to read more, check out this series of posts.

### Defining a `Functor`

interface

Let’s say we want to make a new functor (e.g. `Either`

, `IO`

, or `Task`

). Since we *know* that a functor must implement a `map`

method, we should be able to write an interface for it, right? That would certainly make it easy for us to ensure we’ve created it correctly. Here’s how we could *try* to implement that:

```
// define an inteface for functors
interface Functor<F> {
map: <A, B>(fn: (a: A) => B) => (fa: F<A>) => F<B>;
}
// make some functors with that interface
const ArrayFunctor: Functor<Array> {
map: /* ... */
}
const IdentityFunctor: Functor<Identity> {
map: /* ... */
}
const MaybeFunctor: Functor<Maybe> {
map: /* ... */
}
```

However, this *does not work*!

```
interface Functor<F> {
map: <A, B>(fn: (a: A) => B) => (fa: F<A>) => F<B>;
---- ---- Type 'F' is not generic.
}
```

In this naive attempt, we’re supplying a generic `F`

that we’re trying to use as a generic type itself (`F<A>`

). This is not something that Typescript supports.

### Proposal(s)

One of the oldest proposals I’ve found for this was filed in the Typescript repository *almost a decade ago* (issue). In that proposal, the functor interface would be defined like so:

```
interface Functor<F<~>> {
map: <A, B>(fn: (a: A) => B) => (fa: F<A>) => F<B>;
}
```

In this proposal (which I personally like) we use `F<~>`

to specify that the generic `F`

type takes in a single *parameter*. A generic generic, if you will. This could be expanded to multiple parameters (e.g. `T<~,~>`

to represent generic types that take in two arguments like `Record`

or `Pick`

).

Haskell (one of the languages that *does* support this) calls these “type parameters”.

### Workarounds

This might seem academic and outside of the realm of practical application, but the sheer number of duplicate issues in the Typescript repository and 3rd-party workarounds should hopefully convince you otherwise. One of the more popular solutions is to pull in fp-ts, which requires *a lot* of boilerplate to achieve the desired effect:

```
import { HKT, URIS, Kind } from 'fp-ts/HKT';
type Identity<T> = {};
type Maybe<T> = {};
// merge the "URItoKind" interface from fp-ts with our own stuff
declare module 'fp-ts/HKT' {
interface URItoKind<T> {
'Identity': Identity<T>
'Maybe': Maybe<T>
}
}
interface Functor<F> {
URI: F;
map: <A, B>(fn: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B>;
}
interface Functor1<F extends URIS> {
URI: F;
map: <A, B>(fn: (a: A) => B) => (fa: Kind<F, A>) => Kind<F, B>;
}
// define Functor2 for functors with two type parameters, etc.
const maybeFunctor: Functor1<'Maybe'> = {
URI: 'Maybe',
map: /* ... */
}
```

That’s…a lot of code to get around the problem at hand. Check out this video (the whole series is great) for an explanation of this approach.

### Conclusion

Typescript is a huge improvement over JavaScript, but there are a few powerful features that it simply does not support (yet). I can see how, to the uninitiated, this sort of thing might seem unnecessary or only useful to hard-core functional programmers. However, the same was said of lambda functions or `filter`

, `map`

, and `reduce`

before they made it into the mainstream zeitgeist…now they’re standard in almost every language.