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.
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.
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
).