4

我创建了这个游乐场,代码如下:

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

因为类型没有缩小。

4

6 回答 6

5

这是我经常使用的一种模式(或其变体)。

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)

操场

于 2020-10-01T13:06:24.287 回答
2

你可以通过两种方式做到这一点

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

智能感知工作

于 2020-10-01T07:41:07.273 回答
2

我注意到了这个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),
);

游乐场,因此您可以检查每个州。

fold为您的类型创建处理程序的高阶函数也是如此(与 for 相同Option)。

于 2020-10-03T02:19:20.543 回答
1

您的问题的解决方案是使用 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而不是扩展它。用户代码不会改变。

于 2020-10-01T08:34:03.777 回答
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);

这是游乐场的链接。

于 2020-09-30T15:06:43.480 回答
0

您将需要使用 缩小BundlerState事件处理程序 lambda 中的参数Extract<BundlerState, {type: 'TYPE'}。您希望确保您的参数与事件处理程序映射中的键匹配(例如eventHandlers['TYPE'],类型为(event: Extract<BundlerState, { type: 'TYPE' }>) => any。这可以通过创建一个特殊EventHandlers类型来实现,该类型强制执行键和事件处理程序的 lambda 签名之间的这种关系。

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
};

如果您不想在事件处理程序映射中定义所有可能的事件类型,则可以使用Partial<EventHandlers>类型。

于 2020-10-01T08:58:46.130 回答