typescript discriminated union 的 switch 语句的替代方案
alternative to switch statement for typescript discriminated union
我已经创建了 this playground,这是代码:
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 是一个可区分的联合体,switch 缩小了类型。
问题是它无法扩展,而且大的扩展 switch 语句非常可怕。
有没有更好的方法我可以写这个并且仍然保持 nice 类型缩小?
你不能这样做:
const eventHandlers = {
BUNDLED: (event: BundlerState) => event.type // type is not narrowed
// etc,
};
const logEvent = (event: BundlerState) => eventHandlers['BUNDLED'](event);
因为类型没有缩小。
也许您可以使用处理程序映射,其中键是事件类型(UNBUNDLED、BUILDING 等),值是需要调用的处理程序:
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 eventHandlers = {
UNBUNDLED: (event: BundlerState) => console.log('received bundler start'),
BUILDING: (event: BundlerState) => console.log('build started'),
GREEN: (event: BundlerState) => console.log('received the following bundler warning'),
ERRORED: (event: BundlerState) => console.log("received build error:"),
};
const logEvent = (event: BundlerState) => eventHandlers[event.type](event);
这里link去游乐场。
有两种方法
const eventHandlers = {
BUNDLED: (event: Extract<BundlerState, { type: 'BUILDING' }>) => event.
// etc,
};
或
type BundlerBuildingState = Extract<BundlerState, { type: 'BUILDING' }> // will be { type: "link"; url: string; }
const eventHandlers = {
BUNDLED: (event: BundlerBuildingState) => event.
// etc,
};
您的问题的解决方案是使用 OOP 和多态性。
让BundlerState
成为声明public接口的抽象基础class:
export abstract class BundlerState {
public abstract logEvent(): void;
}
然后为type
的每个值扩展它:
export class UnbundledState extends BundlerState {
public logEvent(): void {
console.log('received bundler start');
}
}
export class BuildingState extends BundlerState {
public constructor(private warnings: BundlerWarning[]) {}
public logEvent(): void {
console.log('build started');
}
}
export class GreenState extends BundlerState {
public constructor(private path: string; private warnings: BundlerWarning[]) {}
public logEvent(): void {
if(event.warnings.length > 0) {
console.log('received the following bundler warning');
for (let warning of event.warnings) {
console.log(warning.message);
}
}
console.log("build successful!");
console.log('manifest ready');
}
}
export class ErroredState extends BundlerState {
public constructor(private error: BundlerError) { }
public logEvent(): void {
console.log("received build error:");
console.log(event.error.message);
}
}
这样可以在不修改现有代码的情况下添加新类型。
用法
用户代码略有改动。而不是:
const state: BUndlerState = { type: 'BUILDING'; warnings: [ warning1, warning2 ] };
logState(state);
变成:
const state: BundlerState = new BuildingState([warning1, warning2]);
state.logState();
进一步讨论
你注意到 属性 type
发生了什么吗?
它消失了(因为不再需要它);它的值现在编码在类型本身中(进入 class 名称)。
OOP 通常(显然)比过程方法产生更多的代码。建议的解决方案有 42 行(包括空行),而原始解决方案只有 33 行。
但是每个 class 可以而且应该保留在自己的文件中。这导致更小的代码片段更容易阅读和理解。
此外,可以添加新类型 BundlerState
(新 classes)(在新文件中)而不更改现有文件。
根本不需要 class;可以改用接口。状态 classes 没有公共属性(字段 type
消失了,因为它不需要)。它们的共同点是一种行为(logEvent()
方法),这可以用接口表示:
interface BundlerState {
logEvent(): void
}
然后每个状态 class 将 implement BundlerState
而不是扩展它。用户密码不变。
您需要使用 Extract<BundlerState, {type: 'TYPE'}
缩小事件处理程序 lambda 中的 BundlerState
参数。你 将 想要确保你的参数匹配事件处理程序映射中的键(例如 eventHandlers['TYPE']
是 (event: Extract<BundlerState, { type: 'TYPE' }>) => any
类型)。这可以通过创建一个特殊的EventHandlers
强制执行键和事件处理程序的 lambda 签名之间这种关系的类型。
使用前面提到的 Extract<...>
方法定义一个类型来缩小 BundlerState
也可以显着减少句法的丑陋。
// generic parameter is optional; if no generic is passed, returns the full BundleState union
type NarrowedBundlerState<T extends BundlerState["type"] = BundlerState["type"]> = Extract<BundlerState, { type: T }>;
// event handler map that ensures a relationship between the key and the event handler's lambda signature
type EventHandlers = { [T in BundlerState["type"]]: (event: NarrowedBundlerState<T>) => any; };
const eventHandlers: EventHandlers = {
// allowed entries; we can also access the narrowed type's properties correctly
UNBUNDLED: (event: NarrowedBundlerState<"UNBUNDLED">) => event.type,
BUILDING: (event: NarrowedBundlerState<"BUILDING">) => event.warnings,
GREEN: (event: NarrowedBundlerState<"GREEN">) => event.path,
ERRORED: (event: NarrowedBundlerState<"ERRORED">) => event.type,
};
const badEventHandlers: Partial<EventHandlers> = {
// a non-allowed entry because the key and 'type' parameter do not match
ERRORED: (event: NarrowedBundlerState<"GREEN">) => event.type,
};
const logEvent = (event: BundlerState) => {
// a caveat is you need to cast the retrieved event handler to a more general event handler lambda signature
(eventHandlers[event.type] as (event: BundlerState) => any)(event);
// alternatively you could cast to (params: NarrowedBundlerState<typeof event.type>) => any
// however, it resolves to (event: BundlerState) => any anyways
};
如果您不想在事件处理程序映射中定义所有可能的事件类型,您可以使用 Partial<EventHandlers>
类型。
这是我经常使用的模式(或其变体)。
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> }
使用上面定义的类型,您可以得到一个非常符合人体工程学的实现,如下所示:
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)
更贴近您的原始定义,甚至更不冗长,您可以这样做:
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)
我注意到了 fp-ts
标签,所以我想我会在考虑该库的情况下给出方法。 fp-ts
定义了很多 fold
操作,这些操作基本上实现了您正在寻找的各种代数类型的结果。一般的想法是定义一个函数来为您缩小范围,然后为每种情况定义处理程序。
简单示例
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"
所以对于你的类型,你可以这样写:
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 这样您就可以检查每个状态。
所以 fold
是一个高阶函数,用于为您的类型创建处理程序(与 Option
的处理方式相同)。
我已经创建了 this playground,这是代码:
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 是一个可区分的联合体,switch 缩小了类型。
问题是它无法扩展,而且大的扩展 switch 语句非常可怕。
有没有更好的方法我可以写这个并且仍然保持 nice 类型缩小?
你不能这样做:
const eventHandlers = {
BUNDLED: (event: BundlerState) => event.type // type is not narrowed
// etc,
};
const logEvent = (event: BundlerState) => eventHandlers['BUNDLED'](event);
因为类型没有缩小。
也许您可以使用处理程序映射,其中键是事件类型(UNBUNDLED、BUILDING 等),值是需要调用的处理程序:
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 eventHandlers = {
UNBUNDLED: (event: BundlerState) => console.log('received bundler start'),
BUILDING: (event: BundlerState) => console.log('build started'),
GREEN: (event: BundlerState) => console.log('received the following bundler warning'),
ERRORED: (event: BundlerState) => console.log("received build error:"),
};
const logEvent = (event: BundlerState) => eventHandlers[event.type](event);
这里link去游乐场。
有两种方法
const eventHandlers = {
BUNDLED: (event: Extract<BundlerState, { type: 'BUILDING' }>) => event.
// etc,
};
或
type BundlerBuildingState = Extract<BundlerState, { type: 'BUILDING' }> // will be { type: "link"; url: string; }
const eventHandlers = {
BUNDLED: (event: BundlerBuildingState) => event.
// etc,
};
您的问题的解决方案是使用 OOP 和多态性。
让BundlerState
成为声明public接口的抽象基础class:
export abstract class BundlerState {
public abstract logEvent(): void;
}
然后为type
的每个值扩展它:
export class UnbundledState extends BundlerState {
public logEvent(): void {
console.log('received bundler start');
}
}
export class BuildingState extends BundlerState {
public constructor(private warnings: BundlerWarning[]) {}
public logEvent(): void {
console.log('build started');
}
}
export class GreenState extends BundlerState {
public constructor(private path: string; private warnings: BundlerWarning[]) {}
public logEvent(): void {
if(event.warnings.length > 0) {
console.log('received the following bundler warning');
for (let warning of event.warnings) {
console.log(warning.message);
}
}
console.log("build successful!");
console.log('manifest ready');
}
}
export class ErroredState extends BundlerState {
public constructor(private error: BundlerError) { }
public logEvent(): void {
console.log("received build error:");
console.log(event.error.message);
}
}
这样可以在不修改现有代码的情况下添加新类型。
用法
用户代码略有改动。而不是:
const state: BUndlerState = { type: 'BUILDING'; warnings: [ warning1, warning2 ] };
logState(state);
变成:
const state: BundlerState = new BuildingState([warning1, warning2]);
state.logState();
进一步讨论
你注意到 属性
type
发生了什么吗?
它消失了(因为不再需要它);它的值现在编码在类型本身中(进入 class 名称)。OOP 通常(显然)比过程方法产生更多的代码。建议的解决方案有 42 行(包括空行),而原始解决方案只有 33 行。
但是每个 class 可以而且应该保留在自己的文件中。这导致更小的代码片段更容易阅读和理解。
此外,可以添加新类型 BundlerState
(新 classes)(在新文件中)而不更改现有文件。
根本不需要 class;可以改用接口。状态 classes 没有公共属性(字段
type
消失了,因为它不需要)。它们的共同点是一种行为(logEvent()
方法),这可以用接口表示:interface BundlerState { logEvent(): void }
然后每个状态 class 将
implement BundlerState
而不是扩展它。用户密码不变。
您需要使用 Extract<BundlerState, {type: 'TYPE'}
缩小事件处理程序 lambda 中的 BundlerState
参数。你 将 想要确保你的参数匹配事件处理程序映射中的键(例如 eventHandlers['TYPE']
是 (event: Extract<BundlerState, { type: 'TYPE' }>) => any
类型)。这可以通过创建一个特殊的EventHandlers
强制执行键和事件处理程序的 lambda 签名之间这种关系的类型。
使用前面提到的 Extract<...>
方法定义一个类型来缩小 BundlerState
也可以显着减少句法的丑陋。
// generic parameter is optional; if no generic is passed, returns the full BundleState union
type NarrowedBundlerState<T extends BundlerState["type"] = BundlerState["type"]> = Extract<BundlerState, { type: T }>;
// event handler map that ensures a relationship between the key and the event handler's lambda signature
type EventHandlers = { [T in BundlerState["type"]]: (event: NarrowedBundlerState<T>) => any; };
const eventHandlers: EventHandlers = {
// allowed entries; we can also access the narrowed type's properties correctly
UNBUNDLED: (event: NarrowedBundlerState<"UNBUNDLED">) => event.type,
BUILDING: (event: NarrowedBundlerState<"BUILDING">) => event.warnings,
GREEN: (event: NarrowedBundlerState<"GREEN">) => event.path,
ERRORED: (event: NarrowedBundlerState<"ERRORED">) => event.type,
};
const badEventHandlers: Partial<EventHandlers> = {
// a non-allowed entry because the key and 'type' parameter do not match
ERRORED: (event: NarrowedBundlerState<"GREEN">) => event.type,
};
const logEvent = (event: BundlerState) => {
// a caveat is you need to cast the retrieved event handler to a more general event handler lambda signature
(eventHandlers[event.type] as (event: BundlerState) => any)(event);
// alternatively you could cast to (params: NarrowedBundlerState<typeof event.type>) => any
// however, it resolves to (event: BundlerState) => any anyways
};
如果您不想在事件处理程序映射中定义所有可能的事件类型,您可以使用 Partial<EventHandlers>
类型。
这是我经常使用的模式(或其变体)。
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> }
使用上面定义的类型,您可以得到一个非常符合人体工程学的实现,如下所示:
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)
更贴近您的原始定义,甚至更不冗长,您可以这样做:
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)
我注意到了 fp-ts
标签,所以我想我会在考虑该库的情况下给出方法。 fp-ts
定义了很多 fold
操作,这些操作基本上实现了您正在寻找的各种代数类型的结果。一般的想法是定义一个函数来为您缩小范围,然后为每种情况定义处理程序。
简单示例
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"
所以对于你的类型,你可以这样写:
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 这样您就可以检查每个状态。
所以 fold
是一个高阶函数,用于为您的类型创建处理程序(与 Option
的处理方式相同)。