haspar.us

Maybe‑ts. A softer approach to optionals

Dec 18, 2019 · WrocTypeScript

Slides: https://maybe-ts.now.sh/
The library: https://github.com/hasparus/maybe-ts
Talk duration: 15 minutes


I spoke about problems with hard Option type (like the one from fp-ts) mentioned in Rich Hickey’s “Maybe Not” and presented a foldable traversable monad instance which can used instead of Option to avoid the problems mentioned by Rich.

Edit: It breaks composition law :(

Thanks to @oliverjash and the FP Slack I learned and @gcanti released the version 0.2.1 of fp-ts-laws 😅.

A functor is a mapping between categories. gcanti’s post on dev.to excellently explains what a category is.

An endofunctor is a functor which maps a category into itself 🔄

An endofunctor in TypeScript is a type F<A> with a map function of type <A, B>(fa: F<A>, f: (a: A) => B) => F<B>.

We’re pretty used to how map works for the most popular functor, the Array. It would be cool if the definition of Functor was limited to the types where map behaves similarly. It is. These are the laws:

1. functors preserve identity:
   F.map(fa, a => a) = fa
2. functors preserve composition:
   F.map(fa, a => bc(ab(a))) = F.map(F.map(fa, ab), bc)

Our type is defined as

type Maybe<T> = T | null | undefined;

Since Maybe<Maybe<T>> collapses to Maybe<T>, we do not preserve composition. It is not a functor over nullables. Similar scenario takes place for Promise and thenables.

Counterexample

const f = (x: string | null) => (x ? x.length : 0);
const g = (y: number) => (y === 10 ? null : String(y * 2));

const left = map(10, (x) => f(g(x)));
const right = map(map(10, g), f);

console.log(left, right); // 0 null

Can we save it?

But hey! I don’t really need Maybe<null | undefined>. It’s useless. What if we could just get rid of these two empty values?

Let’s constrain the generic parameter. We’ll have to say goodbye to the fp-ts HKT, but let’s just try for educational purposes.

export type Nothing = null | undefined;

type NotNothing =
  | string
  | number
  | boolean
  | bigint
  | symbol
  | Function
  | Date
  | Error
  | RegExp;

export type Maybe<T extends NotNothing> = T | Nothing;

Let’s just paste extends NotNothing into every function that’s now glowing red.

A type error that defends us from the counterexample.

Our Maybe is not an endofunctor in TypeScript, but it is a mapping between non-nullable TypeScript to TypeScript! Yeah! It is a functor then! Just not an useful one.

Outline

  • who am I
  • wtf is Option
  • problems with Option in fp-ts
    • small problems
      • fp-ts.Option.None is truthy
      • incompatible with TS syntactic sugar optional chaining (.?) and nullish coalescing (??)
    • big problems (mentioned in Maybe Not)
      • relaxing a requirement should be a compatible change
      • strengthening a promise should be a compatible change
  • a simple solution
    type Maybe<T> = T | null | undefined;
  • implementing a fp-ts typeclass instance to use instead of Option
  • problems solved
    • ✔ relaxing a requirement is a compatible change
    • ✔ strengthening a promise is a compatible change
    • ✔ it just works™ with optional chaining and nullish coalescing operators
    • ✔ Nothing is properly empty value (Nothing == null) === true
  • when would you prefer Option?
    • strictNullChecks: false
    • null (or undefined) is an important part of T

Edited 2 times
  1. 49a3e89Sep 14, 2020More work on brain-style notes
  2. 5b51651Sep 13, 2020Initial work on Brain-style notes