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

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


person thSoft    schedule 10.01.2014    source источник


Ответы (6)


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

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

person thSoft    schedule 12.01.2014
comment
Возможно, вы можете использовать sweetjs для создания макроса для «сопоставления» и обертывания нескольких типов Choice, чтобы у вас была проверка типов, но фактическая реализация была бы предоставлена ​​​​макросом. - person Johnny Everson; 11.07.2014

TypeScript 1.4 добавляет объединения и защита типов.

person thSoft    schedule 12.11.2015

Пример, иллюстрирующий принятый ответ:

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:
    }
}
person Franklin Yu    schedule 17.08.2018
comment
Пример основан на действиях в Redux. - person Franklin Yu; 18.08.2018

Отвечать

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

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

class Foo{}
var x = new Foo();
console.log(x instanceof Foo); // true
person basarat    schedule 10.01.2014
comment
Спасибо за это понимание, это интересно, но не отвечает на сам вопрос, поэтому я предлагаю вместо этого преобразовать его в комментарий. - person thSoft; 10.01.2014

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

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

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

  1. не обеспечивает статического соответствия всем случаям,
  2. не различает разные АТД.

Это выглядит так:

// 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>{});
person Søren Debois    schedule 14.01.2016

Это старый вопрос, но, возможно, это все еще поможет кому-то:

Как и ответ @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. Если кто-нибудь знает, как сжать это, оставьте комментарий ниже!

person Daniel    schedule 15.08.2020