Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 621
dagda1 Avatar asked Dec 22 '22 16:12

dagda1


1 Answers

Here is a pattern (or its variations) I use quite often.

type BundlerStatesDef = {
   UNBUNDLED: {}
   BUILDING: { warnings: BundlerWarning[] }
   GREEN: { path: string; warnings: BundlerWarning[] }
   ERRORED: { error: BundlerError }
}
type BundlerStateT = keyof BundlerStatesDef
type BundlerStates = { [K in BundlerStateT]: { type: K } & BundlerStatesDef[K] }
type BundlerHandler<K extends BundlerStateT> = (params: BundlerStates[K]) => void
type BundlerHandlers = { [K in BundlerStateT]: BundlerHandler<K> }

With types defined as above you could have a pretty ergonomic implementation, like so:

const handlers: BundlerHandlers = {
  UNBUNDLED: params => console.log(params),
  BUILDING: params => console.log(params),
  GREEN: params => console.log(params),
  ERRORED: params => console.log(params)
}

const logEvent = <E extends BundlerStateT>(event: BundlerStates[E]) =>
  (handlers[event.type] as BundlerHandler<E>)(event)

PLAYGROUND


Sticking closer you your original definition and be even less verbose you could do it like that:

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 }

export type BundlerHandlers = { [K in BundlerState['type']]: (params: Extract<BundlerState, { type: K }>) => void }

const handlers: BundlerHandlers = {
  UNBUNDLED: params => console.log(params),
  BUILDING: params => console.log(params),
  GREEN: params => console.log(params),
  ERRORED: params => console.log(params)
}

const logEvent = (event: BundlerState) =>
  (handlers[event.type] as (params: Extract<BundlerState, { type: typeof event['type'] }>) => void )(event)

PLAYGROUND

like image 90
artur grzesiak Avatar answered May 05 '23 07:05

artur grzesiak