The nothing/something dichotomy: nullish types in slotted lists — Domain Specific Language

4 min read Original article ↗
  1. Using nothing in place of something
  2. Slotted list
  3. Borrowing Option

In programming languages with nullish types: null, nil, undefined, and so on, programmers will usually reach for one of the nullish tokens when representing something that can be optional or clearable. Something interesting happens when you combine that form of optionality with another form of optionality: fixed-length lists that represent slots to be filled in.

At work!

The ideas in this post started at work — a discussion, a bug, an insight — and made it all the way here.

Using nothing in place of something

Given an array of User items, it is possible to represent a variable number of users, to do amazing things such as loop over them, render them, print them, etc.

interface User {
    name: string;
}

const users: User[] = [{ name: "A"}, {name: "B"}, { name: "C" }];

An additional requirement may be that it should be possible to display empty, to-be-filled-in users. A naïve way might be to represent the nothing/something dichotomy as a union type where one member is User and the other is nullish; undefined for example.

This is what I describe as using nothing in place of something. Let's declare an instance in the list of users as the union of User and undefined:

const users: (User | undefined)[] = [ ... ];

This is probably fine in many cases.

Slotted list

Let's consider a slotted list. Imagine an array that has a fixed length: the starting lineup for a football team or the number of slots in a toaster. Let your imagination run free. You can have empty slots, and the slots are not filled in any particular order.

const slots: (User | undefined)[] = [
    undefined,
    undefined,
    { name: "A"},
    undefined,
    { name: "B"},
     And so on
];

A problem with this approach is that (User | undefined) can become contagious; a programmer might ask "Why is it necessary to handle undefined everywhere?", and "Should the array be passed as-is or could it be filtered to pass only non-nullish values?".

You will need to pass references to (User | undefined)[] around. At some point the distance between the code that centers around (User | undefined)[], which motivates its existence, and other code — rendering, parsing, API calls — will be large enough that the original meaning gets lost. It is not clear from its type signature why it needs to be able to be undefined.

When an array like this is received and passed on, am I allowed to filter it? The originator knows that it's a slotted list, so it can't be filtered — lest we lose information — but not all clients know that.

What we needed is an approach that expresses something that is stronger than just nullishability.

Borrowing Option

I would use a type like this instead, modelled after Rust's Option type:

export const enum OptionKind {
    Some = 1,
    None,
}

export interface Some<T> {
    kind: OptionKind.Some;
    value: T;
}

export interface None {
    kind: OptionKind.None;
    value: undefined;
}

export type Option<T> = Some<T> | None;

A few trivial helper functions like some<T>(t: T) and none() are missing, and are left as an exercise for the reader.

The virtues of Rust's Option type are well documented, and Option above is very similar, but just to spell everything out

  • None can be matched on
  • None can be used in arrays instead of undefined
  • It is clear that None is a placeholder value

Now we can have this, which is better than before

const users: Option<User>[] = [
    none(),
    none(),
    some({ name: "A"}),
    none(),
    some({ name: "B"}),
     And so on
];

We can now decide that the undefined type variant in T | undefined means "nothing", in other words that it hasn't been assigned a value yet. The None variant of Option similarly means "something", or "placeholder".

The "nothing values" can keep meaning nothing. I'm not fond of JavaScript and TypeScript having both undefined and null, but it is what it is. It might have made sense when JavaScript was (apocryphally) created in 10 days, but I don't have to like it.