Tobias Bergkvist
Tobias Bergkvist

Reputation: 2454

Generic type parameter and monads in TypeScript

I'm currently learning about monads - and trying to use TypeScript to understand them better.

I would like to do the following:

type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
type Unit<M, A> = (a: A) => M<A>

But I'm getting the following TypeScript errors:

error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
                          ~~~~

error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
                                                     ~~~~

error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
                                                               ~~~~

error TS2315: Type 'M' is not generic.
type Unit<M, A> = (a: A) => M<A>
                            ~~~~

Is there a way of specifying that the type parameter M should be generic? Or is there another sane way of doing this?

The idea is that I can later pass for example Maybe in place of M, where Maybe is a generic type:

type Maybe<T> = { kind: 'nothing' } | { kind: 'some', value: T }

Upvotes: 2

Views: 613

Answers (1)

jcalz
jcalz

Reputation: 328398

No, TypeScript does not directly support higher kinded types of the sort necessary to define Bind or Unit. There is a longstanding open feature request at microsoft/TypeScript#1213 asking for this, and it is not clear when or if it will ever be implemented.

TypeScript currently lacks the ability to abstract over type functions. (A type function acts on types the way a regular function acts on values; it takes some input type(s) and produces an output type.) It can represent individual concrete type functions like Array, and you can define new individual concrete type functions like:

type Foo<T> = {x: T} // Foo is a type function

but you cannot talk about type functions in general (there's no Foo by itself to reference), nor can you pass around type functions to other higher order type functions. Since your type parameter M needs to be an arbitrary type function for this to work, there's no way to do it directly.


Note that I said it does not directly support higher kinded types. It is possible to emulate higher kinded types, but all the ways of doing so that I know of are clunky and need a lot of manual intervention and boilerplate code. I don't know that I'd call it "insane", but I also wouldn't necessarily say it's "another sane way of doing this" either.

If you want to use an existing library, you could check out fp-ts. Otherwise a sketch of an implementation could look like:

interface TypeFunction<T> { }
type TypeFunctionName = keyof TypeFunction<any>
type Apply<F extends TypeFunctionName, T> = TypeFunction<T>[F];

For each type function you want to talk about, you need to merge an entry into TypeFunction<T>. For example:

interface TypeFunction<T> {
  Array: Array<T>
}

and

interface TypeFunction<T> {
  Foo: Foo<T>
}

Once you do that you can change any use of M<T> to Apply<M, T> where M is now the key you gave to the type function in TypeFunction<T>. So you can say Apply<"Array", string> and it will be string[], or Apply<"Foo", string> and it will be {x: string}.


Given such a sketch, we can write Bind and Unit like

type Bind<M extends TypeFunctionName> =
  <A>(ma: Apply<M, A>) => <B>(a_mb: ((a: A) => Apply<M, B>)) => Apply<M, B>;
type Unit<M extends TypeFunctionName> =
  <A>(a: A) => Apply<M, A>;

(note how the scope of A and B have changed... you want a single Bind<"Array"> to work for all A and B) and even define other operations like fmap:

const fmap = <M extends TypeFunctionName>(
  bind: Bind<M>, unit: Unit<M>) => <A, B>(f: (a: A) => B) =>
    (ma: Apply<M, A>) => bind(ma)(a => unit(f(a)))

And just for fun we can implement bind and unit for the Array and Foo monads:

namespace ArrayMonad {
  const bind: Bind<"Array"> = ma => mf => ma.flatMap(mf);
  const unit: Unit<"Array"> = a => [a];

  const map = fmap(bind, unit);
  const mapYell = map((x: number) => x + "!");
  const strs = mapYell([1, 2, 3]);
  console.log(strs) // ["1!", "2!", "3!"]
}

namespace FooMonad {
  const bind: Bind<"Foo"> = ma => mf => mf(ma.x);
  const unit: Unit<"Foo"> = a => ({ x: a });

  const map = fmap(bind, unit);
  const mapYell = map((x: number) => x + "!");
  const fooStr = mapYell({ x: 1 });
  console.log(fooStr) // {x: "1!"}
}

Looks good, more or less. I'm not thrilled with having to write Bind<"Foo"> instead of Bind<Foo>, but since there's no Foo to refer to, this is as close as you can currently get.

I'll leave implementing Maybe to you.

Playground link to code

Upvotes: 2

Related Questions