Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you emulate ADTs and pattern matching in TypeScript?

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 support instanceof on interfaces. Which pattern do you use to emulate these language features with maximal type safety and minimal boilerplate code?

like image 434
thSoft Avatar asked Jan 10 '14 00:01

thSoft


3 Answers

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.

like image 74
thSoft Avatar answered Oct 24 '22 14:10

thSoft


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>{});
like image 36
Søren Debois Avatar answered Oct 24 '22 16:10

Søren Debois


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:
    }
}
like image 21
Franklin Yu Avatar answered Oct 24 '22 14:10

Franklin Yu