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?
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.
Here's an alternative to the very good answer by @thSoft. On the plus side, this alternative
{ type : string } & T
, where the shape of T
depends on the value of type
,on the negative side
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>{});
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:
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With