Generic Variants
Use the onTerms
helper function to create generic variants. Let's create the classic Option type, with Some
, which contains data, and None
, which doesn't. Some of you know it as Maybe, with Just
and Nothing
instead. These are an excellent alternative to null handling, which has been referred to by its creator as his billion dollar mistake.
ts
import {variant ,onTerms ,payload ,nil ,TypeNames ,GVariantOf } from 'variant';Âexport constOption =variant (onTerms (({T }) => ({Some :payload (T ),None :nil ,})));export typeOption <T ,TType extendsTypeNames <typeofOption > = undefined>=GVariantOf <typeofOption ,TType , {T :T }>;
ts
import {variant ,onTerms ,payload ,nil ,TypeNames ,GVariantOf } from 'variant';Âexport constOption =variant (onTerms (({T }) => ({Some :payload (T ),None :nil ,})));export typeOption <T ,TType extendsTypeNames <typeofOption > = undefined>=GVariantOf <typeofOption ,TType , {T :T }>;
Here's the type in action.
ts
import {Option } from './option';Âconstnum =Option .Some (4);conststr =Option .Some ('thing');
ts
import {Option } from './option';Âconstnum =Option .Some (4);conststr =Option .Some ('thing');
The Option.Some
function is parameterized, meaning that it will infer its return value from whatever is passed in. Here, num
has is one case of Option<number>
, while str
is one of Option<string>
. More complex types may also be stored, and generic functions can be written to process arbitrary types.
ts
import {constant ,isType ,match } from 'variant';import {Option } from './option';Âexport constextract = <T > (option :Option <T >) =>match (option , {Some : ({payload }) =>payload ,None :constant (undefined ),});ÂconstprobablyFour =extract (Option .Some (4));export functionreturnSomes <T >(options :Option <T >[]) {returnoptions .filter (isType (Option .Some )).map (some =>some .payload )}
ts
import {constant ,isType ,match } from 'variant';import {Option } from './option';Âexport constextract = <T > (option :Option <T >) =>match (option , {Some : ({payload }) =>payload ,None :constant (undefined ),});ÂconstprobablyFour =extract (Option .Some (4));export functionreturnSomes <T >(options :Option <T >[]) {returnoptions .filter (isType (Option .Some )).map (some =>some .payload )}
The returnSomes
function has been correctly typed as returning T[]
without our explicitly saying so.
How does it work?​
The T
property of the parameter is a placeholder - an instance of a variant meant to represent a generic term. The object that we destructured to get T
has a placeholder for each letter of the roman alphabet, 26 in total. onTerms()
brands the return type of the template function with a symbol to indicate the template has generic term placeholders. When this symbol is present, a special overload of variant()
is triggered which will rewrite the T
placeholder with an actualized generic type.
Upcoming: Incorporate tree example properly.
Tree<T>​
Trees will need to be defined type-first like all recursive variants.
ts
const Tree = variant(onTerms(({T}) => {type Tree<T> =| Variant<'Branch', {payload: T, left: Tree<T>, right: Tree<T>}>| Variant<'Leaf', {payload: T}>;return {Branch: fields<{left: Tree<typeof T>, right: Tree<typeof T>, payload: typeof T}>(),Leaf: payload(T),}}));type Tree<T, TType extends TypeNames<typeof Tree> = undefined>= GVariantOf<typeof Tree, TType, {T: T}>;
ts
const Tree = variant(onTerms(({T}) => {type Tree<T> =| Variant<'Branch', {payload: T, left: Tree<T>, right: Tree<T>}>| Variant<'Leaf', {payload: T}>;return {Branch: fields<{left: Tree<typeof T>, right: Tree<typeof T>, payload: typeof T}>(),Leaf: payload(T),}}));type Tree<T, TType extends TypeNames<typeof Tree> = undefined>= GVariantOf<typeof Tree, TType, {T: T}>;
but otherwise follow the same process.
ts
const binTree = Tree.Branch({payload: 1,left: Tree.Branch({payload: 2,left: Tree.Leaf(4),right: Tree.Leaf(5),}),right: Tree.Leaf(3),})function depthFirst<T>(node: Tree<T>): T[] {return match(node, {Leaf: ({payload}) => [payload],Branch: ({payload, left, right}) => {return [payload, ...depthFirst(left), ...depthFirst(right)];}})}const [d1, d2, d3, d4, d5] = depthFirst(binTree);expect(d1).toBe(1);expect(d2).toBe(2);expect(d3).toBe(4);expect(d4).toBe(5);expect(d5).toBe(3);
ts
const binTree = Tree.Branch({payload: 1,left: Tree.Branch({payload: 2,left: Tree.Leaf(4),right: Tree.Leaf(5),}),right: Tree.Leaf(3),})function depthFirst<T>(node: Tree<T>): T[] {return match(node, {Leaf: ({payload}) => [payload],Branch: ({payload, left, right}) => {return [payload, ...depthFirst(left), ...depthFirst(right)];}})}const [d1, d2, d3, d4, d5] = depthFirst(binTree);expect(d1).toBe(1);expect(d2).toBe(2);expect(d3).toBe(4);expect(d4).toBe(5);expect(d5).toBe(3);