handsum
👏 = ✋ ➕ ✋ /* Create sum types with ergonomics equipped */
Quick Tour on handsum
handsum provides a sophisticated way to define sum types in TypeScript with natural ergonomics that integrate seamlessly with the language.
Core Features:
- Type-safe
if checks: Destructure variants by checking truthiness
- Classical
match: Clean, expressive enough pattern matching
- Associated functions: Attach methods directly to your sum types
Creating a Maybe Type
Let's start by defining a classic Maybe<T> = just(T) | nothing type.
import { handsum, type Handsum } from 'handsum';
interface TMaybe<T extends {}> {
just(value: T): Maybe<T>;
nothing(): Maybe<T>;
}
type Maybe<T extends {}> = Handsum<TMaybe<T>>;
const MaybeCtor = <T extends {}>() => handsum<TMaybe<T>>({});
const Maybe = {
just: <T extends {}>(value: T) => MaybeCtor<T>().just(value),
nothing: <T extends {}>() => MaybeCtor<T>().nothing(),
};
Now you can use it:
Maybe.just(42).match({
just(value) {
console.log(`The answer is ${value}`);
},
nothing() {
console.log('nothing');
},
});
Simple and type-safe!
Advanced Topic: impl.
You can associate utility functions like Maybe<T>.map :: ((T) -> U) -> Maybe<U> directly with your type.
import { handsum, type Handsum } from 'handsum';
interface TMaybe<T extends {}> {
just(value: T): Maybe<T>;
nothing(): Maybe<T>;
}
interface IMaybe<T extends {}> {
map<U extends {}>(this: Maybe<T>, f: (value: T) => U): Maybe<U>;
}
type Maybe<T extends {}> = Handsum<TMaybe<T>, IMaybe<T>>;
const MaybeCtor = <T extends {}>() =>
handsum<TMaybe<T>, IMaybe<T>>({
map<U extends {}>(this: Maybe<T>, f: (value: T) => U): Maybe<U> {
return this.match({
just: (value) => MaybeCtor<U>().just(f(value)),
nothing: () => MaybeCtor<U>().nothing(),
});
},
});
const Maybe = {
just: <T extends {}>(value: T) => MaybeCtor<T>().just(value),
nothing: <T extends {}>() => MaybeCtor<T>().nothing(),
};
Now you can use methods naturally:
Maybe.just(42)
.map((x) => x * 2)
.map((x) => x / 2)
Flow-sensitive Matching
For the demonstration, let's say we have following type.
import { handsum, type Handsum } from 'handsum';
type NumbersStr = '00' | '01' | '10' | '11';
type TNumbers = Record<NumbersStr, (val: number) => NumbersStr>;
type Numbers = Handsum<TNumbers>;
const Numbers = handsum<TNumbers>({});
Using if statements
To quickly extract values from specific variants like 00 or 11:
function process(n: Numbers) {
if (n['00']) return n['00'][0];
if (n['11']) return n['11'][0];
Using match(...)
Since 00 and 11 are already handled, we can match the remaining cases:
return n.match({
'01': (x) => x * 2,
'10': (x) => x * 3,
});
}
The special _ wildcard case
Use _ to handle all remaining variants in one place:
function only0011(n: Numbers) {
return n.match({
'00': () => '00',
'11': () => '11',
_: () => 'others',
});
}
Advanced Topic: Subsetting Variants
You can enhance flow sensitivity in subroutines by subsetting variants using HandsumSubset.
function only0011Simpler(n: Numbers) {
if (n['00'] || n['11']) return simpler(n);
return 'others';
}
function simpler(n: HandsumSubset<Numbers, '00' | '11'>) {
return n.match({
'00': () => '00',
'11': () => '11',
});
}
This is an advanced feature that you'll rarely need in typical usage.