繁体   English   中英

你如何在 TypeScript 中模拟 ADT 和模式匹配?

[英]How do you emulate ADTs and pattern matching in TypeScript?

不幸的是,从 0.9.5 开始,TypeScript(还)没有代数数据类型(联合类型)和模式匹配(以解构它们)。 更重要的是,它甚至不支持接口上的 instanceof。 您使用哪种模式来模拟具有最大类型安全性和最少样板代码的这些语言功能?

我去下面的访问状的图案,灵感来自这个这个(在本例中,一个Choice可能是FooBar ):

interface Choice {
    match<T>(cases: ChoiceCases<T>): T;
}

interface ChoiceCases<T> {
    foo(foo: Foo): T;
    bar(bar: Bar): T;
}

class Foo implements Choice {

    match<T>(cases: ChoiceCases<T>): T {
        return cases.foo(this);
    }

}

class Bar implements Choice {

    match<T>(cases: ChoiceCases<T>): T {
        return cases.bar(this);
    }

}

用法:

function getName(choice: Choice): string {
    return choice.match({
        foo: foo => "Foo",
        bar: bar => "Bar",
    });
}

匹配本身具有表现力且类型安全,但需要为类型编写大量样板。

TypeScript 1.4 添加了联合类型和类型保护

举例说明已接受的答案:

enum ActionType { AddItem, RemoveItem, UpdateItem }
type Action =
    {type: ActionType.AddItem, content: string} |
    {type: ActionType.RemoveItem, index: number} |
    {type: ActionType.UpdateItem, index: number, content: string}

function dispatch(action: Action) {
    switch(action.type) {
    case ActionType.AddItem:
        // now TypeScript knows that "action" has only "content" but not "index"
        console.log(action.content);
        break;
    case ActionType.RemoveItem:
        // now TypeScript knows that "action" has only "index" but not "content"
        console.log(action.index);
        break;
    default:
    }
}

回答

它甚至不支持接口上的 instanceof。

原因是类型擦除。 接口只是一种编译类型构造,没有任何运行时影响。 但是,您可以在类上使用 instanceof,例如:

class Foo{}
var x = new Foo();
console.log(x instanceof Foo); // true

这是@thSoft 非常好的答案的替代方案。 从好的方面来说,这个替代方案

  1. 具有与表格上的原始javascript对象潜在的互操作性{ type : string } & T ,其中的形状T取决于的值type
  2. 每个选择的样板文件要少得多;

在消极方面

  1. 不会静态强制您匹配所有情况,
  2. 不区分不同的 ADT。

它看起来像这样:

// One-time boilerplate, used by all cases. 

interface Maybe<T> { value : T }
interface Matcher<T> { (union : Union) : Maybe<T> }

interface Union { type : string }

class Case<T> {
  name : string;
  constructor(name: string) {
    this.name = name;
  }
  _ = (data: T) => ( <Union>({ type : this.name, data : data }) )
  $ =
    <U>(f:(t:T) => U) => (union : Union) =>
        union.type === this.name
          ? { value : f((<any>union).data) }
          : null
}

function match<T>(union : Union, destructors : Matcher<T> [], t : T = null)
{
  for (const destructor of destructors) {
    const option = destructor(union);
    if (option)
      return option.value;
  }
  return t;
}

function any<T>(f:() => T) : Matcher<T> {
  return x => ({ value : f() });
}

// Usage. Define cases.

const A = new Case<number>("A");
const B = new Case<string>("B");

// Construct values.

const a = A._(0);
const b = B._("foo");

// Destruct values.

function f(union : Union) {
  match(union, [
    A.$(x => console.log(`A : ${x}`))
  , B.$(y => console.log(`B : ${y}`))
  , any (() => console.log(`default case`))
  ])
}

f(a);
f(b);
f(<any>{});

这是一个老问题,但也许这仍然会帮助某人:

就像@SorenDebois 的答案一样,这个答案是@theSoft 的每个案例样板的一半。 它也比@Soren 更封装。 此外,此解决方案具有类型安全性、类似switch的行为,并强制您检查所有情况。

// If you want to be able to not check all cases, you can wrap this type in `Partial<...>`
type MapToFuncs<T> = { [K in keyof T]: (v: T[K]) => void }
// This is used to extract the enum value type associated with an enum. 
type ValueOfEnum<_T extends Enum<U>, U = any> = EnumValue<U>

class EnumValue<T> {
  constructor(
    private readonly type: keyof T,
    private readonly value?: T[keyof T]
  ) {}

  switch(then: MapToFuncs<T>) {
    const f = then[this.type] as (v: T[keyof T]) => void
    f(this.value)
  }
}

// tslint:disable-next-line: max-classes-per-file
class Enum<T> {
  case<K extends keyof T>(k: K, v: T[K]) {
    return new EnumValue(k, v)
  }
}

用法:

// Define the enum. We only need to mention the cases once!
const GameState = new Enum<{
  NotStarted: {}
  InProgress: { round: number }
  Ended: {}
}>()

// Some function that checks the game state:
const doSomethingWithState = (state: ValueOfEnum<typeof GameState>) => {
    state.switch({
      Ended: () => { /* One thing */ },
      InProgress: ({ round }) => { /* Two thing with round */ },
      NotStarted: () => { /* Three thing */ },
    })
}

// Calling the function
doSomethingWithState(GameState.case("Ended", {}))

这里真正不理想的一方面是需要ValueOfEnum 在我的申请中,这足以让我接受@theSoft 的回答。 如果有人知道如何压缩它,请在下面发表评论!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM