Home > OS >  Check for exhaustive in a builder pattern
Check for exhaustive in a builder pattern

Time:05-05

I want to build something like this:

type MyValues = "good" | "bad" | "ugly"

function doSomething(values:MyValues) {
   return startBuilder(values)
          .case("good", 1)
          .case("bad", 2)
          .exhaustive() // this should give an compiler error, since I forgot the `ugly` }

I tried to figure out the tail using TypeScript's infer - but with no luck:

type Extract<T, A> = A extends T
  ? T extends infer Tail
    ? Tail
    : never
  : never;
type X = Extract<Values, 'good'>;

But this does not work. Any ideas?

Here is a playground: https://stackblitz.com/edit/typescript-mguypt

CodePudding user response:

We'll first need a type that checks if two types are EXACTLY equal, a simple extends won't do the trick here:

type Equals<A, B> = (<T>() => T extends A ? true : false) extends (<T>() => T extends B ? true : false) ? true : false;

The explanation behind why this interesting-looking type works can be found here by jcalz no less.

Then we define the Builder class:

class Builder<T, SoFar extends ReadonlyArray<unknown> = []> {
    private cases = new Map<T, unknown>();
}

It takes a generic T which is what it will check its cases against, and a generic not intended to be passed by the end user. This generic SoFar stores which cases have been used so far.

So let's build the case function now:

    case<V extends T>(v: V, d: unknown): Builder<T, [...SoFar, V]> {
        this.cases.set(v, d);

        return this as Builder<T, [...SoFar, V]>;
    }

It takes something that extends T and any value, then returns the builder as Builder<T, [...SoFar, V]>, which means we add the case to the cases used up so far.

Finally in here:

    exhaustive(...args: Equals<T, SoFar[number]> extends true ? [] : [{ error: "Builder is not exhaustive." }]) {

    }

We check if T and SoFar[number] are exactly the same. If they are, then we have exhausted all cases. Otherwise, we say that we expect a parameter.

Because you will likely call exhaustive with no parameters, calling it without using up all cases will display a nice error message for the user.

You can even go one step further and include all the missing cases in the error message! The second playground link demonstrates this.

Playground

Playground with error messages that display missing cases

  • Related