Search code examples
typescriptdiscriminated-unionfp-ts

alternative to switch statement for typescript discriminated union


I have created this playground and here is the code:

type BundlerError = Error;
type BundlerWarning = Error;

export type BundlerState =
  | { type: 'UNBUNDLED' }
  | { type: 'BUILDING'; warnings: BundlerWarning[] }
  | { type: 'GREEN'; path: string;  warnings: BundlerWarning[] }
  | { type: 'ERRORED'; error: BundlerError }

const logEvent = (event: BundlerState) => {
    switch (event.type) {
      case 'UNBUNDLED': {
        console.log('received bundler start');
        break;
      }
      case 'BUILDING':
        console.log('build started');
        break;
      case 'GREEN':
        if(event.warnings.length > 0) {
          console.log('received the following bundler warning');

          for (let warning of event.warnings) {
              warning
            console.log(warning.message);
          }
        }
        console.log("build successful!");
        console.log('manifest ready');
        break;
      case 'ERRORED':
        console.log("received build error:");
        console.log(event.error.message);
        break;
    }
}

BundlerState is a discriminated union and the switch narrows the type.

The problem is that it does not scale and big expanding switch statements are pretty horrible.

Is there a better way I can write this and still keep the nice type narrowing?

You cannot do this:

const eventHandlers = {
  BUNDLED: (event: BundlerState) => event.type // type is not narrowed
  // etc,
};

const logEvent = (event: BundlerState) => eventHandlers['BUNDLED'](event);

Because the type is not narrowed.


Solution

  • I noticed the fp-ts tag so I figure I'll give the approach with that library in mind. fp-ts defines a lot of fold operations that achieve essentially the result you're looking for for their various algebraic types. The general idea is to define a function that does the narrowing for you, then you define handlers for each of the cases.

    Simple Example

    import { Option, some, none, fold } from 'fp-ts/lib/Option';
    const x: Option<number> = some(1);
    const y: Option<number> = none;
    
    const printSomeNumber = fold(
      () => console.log('No number'),
      (n) => console.log(n);
    );
    
    printSomeNumber(x); // Logs 1
    printSomeNumber(y); // Logs "No number" 
    

    So for your type, you could write something like this:

    import { absurd } from 'fp-ts';
    type BundlerError = Error;
    type BundlerWarning = Error;
    enum StateType {
      Unbundled = 'UNBUNDLED',
      Building = 'BUILDING',
      Green = 'GREEN',
      Errored = 'ERRORED',
    }
    type Unbundled = { type: StateType.Unbundled; };
    type Building = { type: StateType.Building; warnings: BundlerWarning[]; };
    type Green = { type: StateType.Green; path: string; warnings: BundlerWarning[]; };
    type Errored = { type: StateType.Errored; error: BundlerError };
    export type BundlerState = Unbundled | Building | Green | Errored;
    
    const fold = <ReturnType extends any>(
      a: (state: Unbundled) => ReturnType,
      b: (state: Building) => ReturnType,
      c: (state: Green) => ReturnType,
      d: (state: Errored) => ReturnType,
    ) => (state: BundlerState): ReturnType => {
      switch(state.type) {
        case StateType.Unbundled:
            return a(state);
        case StateType.Building:
            return b(state);
        case StateType.Green:
            return c(state);
        case StateType.Errored:
            return d(state);
        default:
            // This is a helper from fp-ts for throwing when the value should be never.
            return absurd(state);
      }
    };
    
    const logType = fold(
        (state) => console.log(state.type),
        (state) => console.log(state.type),
        (state) => console.log(state.type),
        (state) => console.log(state.type),
    );
    

    Playground so you can inspect each of the states.

    So fold is a higher order function for creating a handler for your type (in the same way as it is for Option).