Question

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?

Was it helpful?

Solution 2

TypeScript 1.4 adds union types and type guards.

OTHER TIPS

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.

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

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

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>{});
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top