打字稿区分联合的switch语句的替代方法

时间:2020-09-27 19:57:02

标签: typescript fp-ts

我已经创建了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);

因为类型没有缩小。

6 个答案:

答案 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)

documentation


让您更接近您的原始定义,而不再那么冗长,您可以这样做:

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

答案 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,
};

Intellisense working

答案 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 }; 类型。