我已经创建了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语句非常可怕。
有没有一种更好的方法可以编写此代码,并且仍然可以缩小类型?
您不能这样做:
const eventHandlers = {
BUNDLED: (event: BundlerState) => event.type // type is not narrowed
// etc,
};
const logEvent = (event: BundlerState) => eventHandlers['BUNDLED'](event);
因为类型没有缩小。
答案 0 :(得分:4)
这是我经常使用的模式(或其变化形式)。
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)
答案 1 :(得分:1)
您可以通过两种方式完成
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,
};
答案 2 :(得分:1)
我注意到了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
的创建方法相同)。
答案 3 :(得分:0)
也许您可以使用处理程序映射,其中键是事件类型(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到操场上。
答案 4 :(得分:0)
您的问题的解决方案是使用OOP和多态性。
让BundlerState
是声明公共接口的抽象基类:
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
发生了什么?
它消失了(因为不再需要它了);现在,其值已编码为类型本身(即类名)。
OOP通常(显然)比过程方法生成更多的代码。建议的解决方案有42行(包括空行),而原始解决方案只有33行。
但是每个类都可以并且应该保留在自己的文件中。这样会导致更小的代码段,更易于阅读和理解。
此外,可以在不更改现有文件的情况下(在新文件中)添加新类型的BundlerState
(新类)。
甚至不需要基类。可以改用接口。状态类没有公共属性(字段type
消失了,因为它不需要)。它们的共同点是一种行为(logEvent()
方法),这可以通过接口表示:
interface BundlerState {
logEvent(): void
}
然后,每个状态类将implement BundlerState
而不是对其进行扩展。用户代码不变。
答案 5 :(得分:0)
您将需要使用BundlerState
来缩小事件处理程序lambda中的Extract<BundlerState, {type: 'TYPE'}
参数。您将想要确保您的参数与事件处理程序映射中的键匹配(例如eventHandlers['TYPE']
的类型为(event: Extract<BundlerState, { type: 'TYPE' }>) => any
。这可以通过创建特殊的{{1 }}类型,用于在密钥和事件处理程序的lambda签名之间建立这种关系。
通过使用前面提到的EventHandlers
方法定义缩小BundlerState
的类型,还可以显着减少语法上的丑陋。
Extract<...>
如果您不想在事件处理程序映射中定义所有可能的事件类型,则可以使用// 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
};
类型。