К сожалению, начиная с 0.9.5, TypeScript (пока) не имеет алгебраических типов данных (типов объединения) и сопоставления с образцом (чтобы их деструктурировать). Более того, он даже не поддерживает instanceof на интерфейсах. Какой шаблон вы используете для эмуляции этих функций языка с максимальной безопасностью типов и минимальным количеством шаблонного кода?
Как вы эмулируете ADT и сопоставление с образцом в TypeScript?
Ответы (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",
});
}
Сопоставление само по себе выразительно и безопасно для типов, но для типов нужно написать много шаблонов.
Пример, иллюстрирующий принятый ответ:
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:
}
}
Отвечать
он даже не поддерживает instanceof на интерфейсах.
Причина - стирание типа. Интерфейсы представляют собой только конструкцию типа компиляции и не имеют никаких последствий во время выполнения. Однако вы можете использовать instanceof для классов, например. :
class Foo{}
var x = new Foo();
console.log(x instanceof Foo); // true
Вот альтернатива очень хорошему ответу @thSoft. С положительной стороны, эта альтернатива
- имеет потенциальную совместимость с необработанными объектами javascript в форме
{ type : string } & T
, где формаT
зависит от значенияtype
, - имеет значительно меньше шаблонов для выбора;
с отрицательной стороны
- не обеспечивает статического соответствия всем случаям,
- не различает разные АТД.
Это выглядит так:
// 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>{});
Это старый вопрос, но, возможно, это все еще поможет кому-то:
Как и ответ @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. Если кто-нибудь знает, как сжать это, оставьте комментарий ниже!