How do you emulate ADTs and pattern matching in TypeScript?

前端 未结 6 1363
清酒与你
清酒与你 2020-12-20 13:53

Unfortunately, as of 0.9.5, TypeScript doesn\'t (yet) have algebraic data types (union types) and pattern matching (to destructure them). What\'s more, it doesn\'t even supp

相关标签:
6条回答
  • 2020-12-20 14:10

    I went with the following Visitor-like pattern, inspired by this and this (in the example, a Choice can be Foo or Bar):

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

    Usage:

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

    The matching itself is expressive and type-safe, but there's lot of boilerplate to write for the types.

    0 讨论(0)
  • 2020-12-20 14:13

    TypeScript 1.4 adds union types and type guards.

    0 讨论(0)
  • 2020-12-20 14:14

    Here's an alternative to the very good answer by @thSoft. On the plus side, this alternative

    1. has potential interoperability with raw javascript objects on the form { type : string } & T, where the shape of T depends on the value of type,
    2. has substantially less per-choice boilerplate;

    on the negative side

    1. does not enforce statically that you match all cases,
    2. does not distinguish between different ADTs.

    It looks like this:

    // 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>{});
    
    0 讨论(0)
  • 2020-12-20 14:18

    To answer

    it doesn't even support instanceof on interfaces.

    Reason is type erasure. Interfaces are a compile type construct only and don't have any runtime implications. However you can use instanceof on classes e.g. :

    class Foo{}
    var x = new Foo();
    console.log(x instanceof Foo); // true
    
    0 讨论(0)
  • 2020-12-20 14:21

    This is an old question, but maybe this will still help someone:

    Like @SorenDebois's answer, this one has half of the per-case boilerplate as @theSoft's. It is also more encapsulated than @Soren's. Additionally, this solution has type safety, switch-like behavior, and forces you to check all cases.

    // 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)
      }
    }
    

    Usage:

    // 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", {}))
    

    The one aspect here that is really not ideal is the need for ValueOfEnum. In my application, that was enough for me to go with @theSoft's answer. If anyone knows how to compress this, drop a comment below!

    0 讨论(0)
  • 2020-12-20 14:26

    Example to illustrate the accepted answer:

    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:
        }
    }
    
    0 讨论(0)
提交回复
热议问题