prelude.ts

The first WIP commit for this thing landed in 2019. Before that it was not really a repository so much as a loose collection of files: snippets, tiny abstractions, examples I would pull up when I wanted to show someone why some supposedly esoteric functional programming concept was not actually that exotic. The code barely changed over the years. I just kept reaching for it, and at some point I had to admit it deserved a proper cleanup.

That is what prelude is now. Not a serious attempt at building the definitive TypeScript FP library. Not a competitor to fp-ts, and certainly not an effect system. It is closer to a reference, or maybe a notebook that finally got promoted cleaned up.

The basic claim behind it has not changed since I started collecting these files: TypeScript/JavaScript (and many other languages) already contain a surprising amount of functional programming — if you squint.

Squinting

Take Array. If you use map, you are working with a functor. Chain flatMap calls and you are doing the thing that sounds intimidating when someone calls it "monadic bind":

// Array.map — this is a functor
[1, 2, 3].map(x => x + 1) // [2, 3, 4]

// Array.flatMap — this is monadic bind
[1, 2, 3].flatMap(x => [x, x * 10]) // [1, 10, 2, 20, 3, 30]

Array is a surprisingly clean example. It has map, it has flatMap, it satisfies the functor and monad laws. Most people just do not call it that.

Promise is the one that gets talked about as monadic quite often, but it is not. Promise.prototype.then looks like monadic bind, but Promise does not satisfy the monad laws. The problem is auto-unwrapping:

// You would expect this to be Promise<Promise<number>>
// instead it collapses to Promise<number>
const nested = Promise.resolve(Promise.resolve(42))

You cannot represent Promise<Promise<T>>. It flattened whether you want that or not. That means you lose the ability to distinguish between transforming a value inside a Promise (map) and sequencing one async operation after another (flatMap).

If you have ever tried to compose Promise-returning functions generically and found yourself fighting the language, that is why. The container swallows the nesting, and with it the information about what kind of composition you meant. And the naming does not help either. There is no shared vocabulary across Objects at the language level.

So, some of the patterns are there if you squint.

Pragmatic, not pure

Libraries like fp-ts emulate higher-kinded types with type-level URIs and a registration mechanism. That is genuinely impressive engineering. But I'd argue the type machinery ends up obscuring the simplicity and beauty of what is underneath. If the type system looks that complicated, you lose a lot of the benefit FP brings to the table.

So with prelude I went a different way:

// Maybe — just a union
type Maybe<A> = Just<A> | Nothing<A>

// Either — just a union
type Either<L, A> = Left<L, A> | Right<L, A>

Plain unions. Where fp-ts and other approaches would go an extra mile to verify things through the type system, prelude verifies them with property-based tests — jsverify checks against functor and monad laws, lens laws, execution behavior. That is a crutch. But it might be truer to the TypeScript design principles 1 and does not force the language into something it's not meant to be.

It also means the patterns are pretty portable. I have pulled things from the project into Python for working with async iterators, and some other places and it was mostly mechanical.

Composition, composition

The whole point of functional programming, in practical terms, is composition. Building small pieces that snap well together. Types like Functor and Monad are not just abstract math, they describe the shape of the connection points between those pieces. If your pieces follow the shape, they compose. If they do not, you find out at the boundary rather than deep inside a call stack. The category theory behind this sounds intimidating, but as Bartosz Milewski argues 2, it is really about the kind of structure that makes programs composable — and that is something programmers already think about every day. Treat it as a design problem and the abstraction falls away.

That is what prelude does. It includes data types — Maybe, Either, Task, Pipeline — and utilities like curry, pipe, compose. Most of them are small enough that looking at the implementation is easier than reasoning about the category theory side of things.

Either is two classes:

export class Left<L, A> implements Monad<A> {
  private constructor(readonly value: L) {}

  public map<B>(f: (a: A) => B): Either<L, B> {
    return this as any;
  }

  public flatMap<B>(f: (a: A) => B | Either<L, B>): Either<L, B> {
    return this as any;
  }

  // ...
}

export class Right<L, A> implements Monad<A> {
  private constructor(readonly value: A) {}

  public map<B>(f: (a: A) => B): Either<L, B> {
    return Right.of<L, B>(f(this.value));
  }

  public flatMap<B>(f: (a: A) => B | Either<L, B>): Either<L, B> {
    const b: B | Either<L, B> = f(this.value);

    if (isEither(b)) {
      return b as Either<L, B>;
    }

    return Right.of<L, B>(b as B);
  }

  // ...
}

That is it. Left skips everything. Right applies the function to the value. Allowing you to chain operations that might fail, e.g. when validating input data — each check is a standalone function that returns Either, short-circuiting on the first Left:

const validateName = (user: User): Either<string, User> =>
  user.name.trim().length >= 2
    ? right(user)
    : left("Name must be at least 2 characters");
 
const validateEmail = (user: User): Either<string, User> =>
  user.email.includes("@")
    ? right(user)
    : left("Email must contain @");
 
const validateAge = (user: User): Either<string, User> =>
  user.age >= 18
    ? right(user)
    : left(`Must be 18+, got ${user.age}`);
 ß
const validateUser = (user: User): Either<string, User> =>
  right<string, User>(user)
    .flatMap(validateName)
    .flatMap(validateEmail)
    .flatMap(validateAge);

Need a different validation order? Rearrange the chain. Need a subset of checks for a different endpoint? Pick the ones you want. Plus each check can be tested nicely in isolation.

The more of these building blocks you accumulate, the easier it gets to combine them for new features and requirements. Professor Frisby's Mostly Adequate Guide makes that case well. Keep it small, keep it concrete, let the shapes do the work.


I'm not planning to grow prelude into an actively maintained library. It is a cleaned-up record of a set of files I have found useful over years.

If you are working with TS/JS and curious about FP, take a look. Brian Lonsdorf's Hardcore Functional Programming in JavaScript on Frontend Masters is a great place to go deeper, it is hands-on and practical in exactly the right way. Beyond that, there is no shortage of good material. Milewski's Category Theory for Programmers, the mentioned Mostly Adequate Guide, and many other books on functional programming — a lot of it freely available online.

I hope this post and the repo demonstrate that none of this is as esoteric or as complicated as it gets made out to be. The theory is worth looking into. It will change how you think about designing things that compose.

You find the repo here: https://github.com/orlandohohmeier/prelude

References

  1. The TypeScript Design Goals, state that a sound type system is none-goal, and that the aim is instead, to strike a balance between correctness and productivity.

  2. In the preface of Category Theory for Programmers by Bartosz Milewski, he argues that category theory deals with the kind of structure that makes programs composable. And that composition is the essence of programming. And I full heartly agree!