ts-union
Tiny library (1Kb unzipped) for algebraic sum types that looks similar to swift enums. Inspired by unionize and F# discriminated-unions
Installation
npm add ts-union
NOTE: uses features from typescript 2.8
Usage
Create
import { Union, of } from 'ts-union';
const PaymentMethod = Union({
Cash: of<void>(),
Check: of<CheckNumber>(),
CreditCard: of<CardType, CardNumber>()
});
type CheckNumber = number;
type CardType = 'MasterCard' | 'Visa';
type CardNumber = string;
Type constructors
const cash = PaymentMethod.Cash();
const check = PaymentMethod.Check(15566909);
const card = PaymentMethod.CreditCard('Visa', '1111-566-...');
const { Cash, Check, CreditCard } = PaymentMethod;
const anotherCheck = Check(566541123);
Matching
const str = PaymentMethod.match(cash, {
Cash: () => 'cash',
Check: n => `check num: ${n.toString()}`,
CreditCard: (type, n) => `${type} ${n}`
});
Also supports deferred (curried) matching and default
case.
const toStr = PaymentMethod.match({
Cash: () => 'cash',
default: _v => 'not cash'
});
const str = toStr(card);
if (aka simplified match)
const str = PaymentMethod.if.Cash(cash, () => 'cash');
You can provide else case as well. In that case 'undefined' type will be removed.
const str = PaymentMethod.if.Check(
cash,
n => `check num: ${n.toString()}`,
_v => 'not check'
);
Type of resulted objects
At the moment types of union values are opaque. That allows me to experiment with different underlying data structures.
type CashType = typeof cash;
The OpaqueUnion<...>
type for PaymentMethod
is accessible via phantom property T
type PaymentMethodType = typeof PaymentMethod.T;
Api and implementation details
If you will try to log a union value you will see just an array.
console.log(PaymentMethod.Check(15566909));
All union values are arrays. The first element is the key to match and the second is payload. I decided not to expose that through typings but I might reconsider that in the future. Although you cannot use it for redux action you can safely use it for redux state.
Api
How to define shape
const U = Union({
Simple: of(),
One: of<string>(),
Const: of(3),
Two: of<string, number>(),
Three: of<string, number, boolean>()
});
Let's take a closer look at of
function
export declare type Types = {
<T = void>(): T extends void ? NoData : One<T>;
<T>(val: T): Const<T>;
<T1, T2>(): Two<T1, T2>;
<T1, T2, T3>(): Three<T1, T2, T3>;
};
export declare const of: Types;
the actual implementation is pretty simple:
export const of: Types = ((val: any) => val) as any;
We just capture the constant and don't really care about the rest. Typescript will guide us to provide proper number of args for each case.
match
accepts either a full set of props or a subset with a default case.
export type MatchFunc<Record> = {
<Result>(cases: MatchCases<Record, Result>): (
val: OpaqueUnion<Record>
) => Result;
<Result>(val: OpaqueUnion<Record>, cases: MatchCases<Record, Result>): Result;
};
if
either accepts a function that will be invoked (with a match) and/or else case.
{
<R>(val: OpaqueUnion<Rec>, f: (a: A) => R): R | undefined;
<R>(val: OpaqueUnion<Rec>, f: (a: A) => R, els: (v: OpaqueUnion<Rec>) => R): R;
}
And that is the whole api.
Breaking changes from 1.1
t
function to define shapes is renamed to 'of'.- There is a different underlying data structure. So if you persisted the values somewhere it won't be compatible with the new version.
The actual change is pretty simple:
type OldShape = [string, ...payload[any]];
type NewShape = [string, payload[any]];
const example = ["Check", [15654747]];