Как вы эмулируете ADT и соответствие шаблонов в TypeScript?

К сожалению, с 0.9.5 TypeScript не имеет (пока) алгебраических типов данных (типы объединения) и сопоставления шаблонов (для их разрушения). Что еще, он даже не поддерживает instanceof на интерфейсах. Какую модель вы используете для эмуляции этих языковых функций с максимальной безопасностью типа и минимальным шаблоном?

Ответ 2

Я пошел со следующим шаблоном, похожим на посетителя, вдохновленным этим и этим (в примере, a Choice может быть Foo или 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);
    }

}

Использование:

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

Соответствие само по себе является выразительным и безопасным по типу, но есть много шаблонов для записи для типов.

Ответ 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

Чтобы ответить

он даже не поддерживает instanceof на интерфейсах.

Причина - стирание типа. Интерфейсы являются только конструкцией типа компиляции и не имеют каких-либо последствий во время выполнения. Однако вы можете использовать instanceof для классов, например.

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

Ответ 5

Здесь альтернатива очень хорошему ответу @thSoft. С положительной стороны, эта альтернатива

  • имеет потенциальную возможность взаимодействия с необработанными объектами javascript в форме { type : string } & T, где форма T зависит от значения type,
  • имеет существенно меньшую конфигурацию для каждого выбора;

на отрицательной стороне

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