A library for building and using union data types(a.k.a algebraic data types) in JS
after using similar libraries such as union-type in my flux/redux stores, I found that I often needed something that could do just a little more.
This library is designed with the following goals:
- be able to, when given bad input values, produce higher quality human readable error messages with more context
- to have a large built in set of validators, but also be support any user supplied validator with the same level of quality and contextual errors
- to be able to provide default values, instead of just throw an error
- to be able to check union types:
- check if 'ANY' type of union
- check if 'ONE OF' a specific set of unions
- check if 'A' specific subtype
To create a union type in union-builder, we must first create a union factory
function isNumber(n) {
return typeof n === 'number';
}
var Points = Type({
Point: [isNumber, isNumber], //using ordered arguments
PointXY: {x: isNumber, y: isNumber} //using named arguments
});
//which can then be used to create union instances
var point = new Points.Point(2, 4)
var point2 = Points.PointXY({y: 4, x: 2});
values can be retrieved from a union instance as follows:
//for record types, instances just use the prop name
point2.x === 2;
point2.y === 4;
point[0] === 2
point[1] === 4
//or
//for ordered arguments, use iterator protocol
for (let value of point) {
//first iteration
value === 2;
}
//Using the destructuring assignment in ECMAScript 6 it is possible to
//concisely extract all fields of a type.
var [x, y] = point;
var { x, y } = point2;
Union Factories accept any function that returns a boolean. Additionally, union-builder will recognize JS primitives and automatically validate them
function isAny() {
return true;
}
var Point = Type({
Any: [isAny], // use your own custom validator
Point: [Number, Number],// js primitives are detected
LazyPoint: [Promise] // even advanced types are recognized
});
// Unions can also be used recursively
var Shape = Type({
//using the built in type or subtype helper
Rectangle: [Point.isType, Point.Point.isType],
//or using the subtype directly
Circle: [Number, Point]
});
there's support for the following primitives
- String
- Number
- Boolean
- Array
- Object
- Function
- RegExp
- Date
- Promise
- Error
validators can also provide default values
function myValidator(propValue, propName, instanceName, instanceObj) {
//propValue === null
//propName === "x"
//instanceName === "Type"
if (!propValue) {
instanceObj[propName] = 0;
}
//additionally, very precise error messages can be thrown
if (propValue === Infinity) {
throw new TypeError(`${instanceName} expected an integer for property ${propName}, but got: ${propValue}`)
}
}
var Record = Union({
WithX: {x: myValidator},
WithY: {x:myValidator, y:Number}
});
var rec = new Record.WithX({y: 10 });
rec.x === 0;
rec.y === 10;
var Action = Union({
Create: [],
Delete: []
});
function reducer(state, action) {
//detect if this is a union type we know how to handle
if (!Action.isType(action))
return state;
//detect if this is a specific subtype
if (Action.Create.isType(action))
return addItem(state);
if (Action.Delete.isType(action))
return removeItem(state);
}
// it is also possible to perform high level checks
function reducer(state, action) {
if(!Union.isUnionType(action)) {
throw new TypeError("conventions require that all actions are a type of Union")
}
}
var Actions = Union({
Update: {x: Number, y: Number},
Create: [isAny],
Delete: [isAny]
});
//case takes 2 arguments, but it also curried. so we can start with only 1
const updateState = Actions.case({
Update: ({x, y}) => Object.assign(state, {x, y}),
Create: () => ({x:0, y:0}),
Delete: () => ({})
})
function reducer(state, action) {
return updateState(action);
}
//wildcard or default fallbacks are also supported
function reducer(state, action) {
return Actions.case({
Update: ({x, y}) => Object.assign(state, {x, y}),
_: () => state,
// or
"*": () => state
}, action);
}
It is also possible to add shared methods to the instances. A Maybe type with a map function could thus be defined as follows:
var isAny = () => true;
var Maybe = Type({Just: [isAny], Nothing: []});
Maybe.prototype.map = function(fn) {
return Maybe.case({
Nothing: () => Maybe.Nothing(),
Just: (v) => Maybe.Just(fn(v))
}, this);
};
var just = Maybe.Just(1);
var nothing = Maybe.Nothing();
nothing.map(add(1)); // => Nothing
just.map(add(1)); // => Just(2)
It is possible to define recursive union types. In the example below, List
is
being used in it's own definition, thus it is still undefined
when being
passed to Type
. Therefore Type
interprets undefined
as being a recursive
invocation of the type currently being defined.
var List = Type({Nil: [], Cons: [R.T, List]});
We can write a function that recursively prints the content of our cons list.
var toString = List.case({
Cons: (head, tail) => head + ' : ' + toString(tail),
Nil: () => 'Nil',
});
var list = List.Cons(1, List.Cons(2, List.Cons(3, List.Nil())));
console.log(toString(list)); // => '1 : 2 : 3 : Nil'