【问题标题】:How do you emulate ADTs and pattern matching in TypeScript?你如何在 TypeScript 中模拟 ADT 和模式匹配?
【发布时间】:2017-10-05 14:57:21
【问题描述】:

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

【问题讨论】:

    标签: pattern-matching typescript algebraic-data-types


    【解决方案1】:

    我采用了以下类似访客的模式,灵感来自 thisthis(在示例中,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",
        });
    }
    

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

    【讨论】:

    • 也许您可以使用 sweetjs 为“匹配”创建一个宏并包装多种类型的选择,以便您进行类型检查,但实际实现将由宏提供。
    【解决方案2】:

    TypeScript 1.4 添加了union types and type guards

    【讨论】:

      【解决方案3】:

      举例说明接受的答案:

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

      【讨论】:

      【解决方案4】:

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

      1. { type : string } &amp; T 形式的原始javascript 对象具有潜在的互操作性,其中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>{});
      

      【讨论】:

        【解决方案5】:

        回答

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

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

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

        【讨论】:

        • 感谢您的见解,这很有趣,但它本身并不能回答问题,因此我建议将其转换为评论。
        【解决方案6】:

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

        就像@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 的回答。如果有人知道如何压缩这个,请在​​下面发表评论!

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-04-06
          • 2011-04-02
          • 2018-08-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-08-31
          相关资源
          最近更新 更多