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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With