• Stars
    star
    477
  • Rank 92,112 (Top 2 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 9 years ago
  • Updated over 5 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A small JavaScript library for defining and using union types.

union-type

A small JavaScript library for defining and using union types.

Union types are a way to group different values together. You can think of them as a powerful form of enums with the possibility to have additional data associated with the possible values.

Table of contents

Tutorial

Defining a union type

union-type exports a single function Type. Union types are created by passing the Type function a definition object. The easiest way to define a Type is as follows:

function isNumber(n) { return typeof n === 'number'; }
var Point = Type({Point: [isNumber, isNumber]});

The keys of the object are the names of the values that the type can have. The values of the object are arrays describing the fields of the value. The fields can be described by a validator function. When a value of the type is constructed the values passed to the constructor will have to pass the validator predicate.

Alternatively the fields can be specified by one of the standard built-in constructors Number, String, Object, Array or Function. union-type will detect these constructors and convert them to matching validator functions. Thus the above example is equivalent to this:

var Point = Type({Point: [Number, Number]});

Records

Instead of supplying only the types of the individual constructors it is also possible to define records using object descriptions:

var Point = Type({Point: {x: Number, y: Number}});

Instance methods

Furthermore it is possible to add instance methods. A Maybe type with a map function could thus be defined as follows:

var add = first => second => first + second;  

var T = function () { return true; };
var Maybe = Type({Just: [T], 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)

Finally fields can be described in terms of other types.

var Shape = Type({
  Circle: [Number, Point],
  Rectangle: [Point, Point]
});

The values of a type can also have no fields at all.

var NotifySetting = Type({Mute: [], Vibrate: [], Sound: [Number]});

Constructing a union type

The Type function returns an object with constructor function for the different specified values. Thus, once you've defined a union type like this

var Point = Type({Point: [Number, Number]});
var Shape = Type({
  Circle: [Number, Point],
  Rectangle: [Point, Point]
});

You can create values like this:

var center = Point.Point(12, 7);
var radius = 8;
var circle = Shape.Circle(radius, center);

If you in any way pass a field value that does not match the specification a helpful error is thrown.

var p = Point.Point('foo', 4);
// throws TypeError: bad value 'foo' passed to first argument of constructor Point

As mentioned earlier you can also define records using object descriptions:

var Point = Type({Point: {x: Number, y: Number}});

Types defined using the record syntax have to be constructed using the respective <name>Of constructor. The Point type above is hence constructed using PointOf:

var p = Point.PointOf({x: 1, y: 1});

Alternatively records can be constructed in the same way as regular types.

var p = Point.Point(1, 1);

Switching on union types

Every created type has a case function available along with its value constructors. case can be used as a control structure for handling the different values a type can have:

var Action = Type({Up: [], Right: [], Down: [], Left: [], Jump: [], Fire: [Number]});

var player = {x: 0, y: 0};

var advancePlayer = function(action, player) {
  return Action.case({
    Up: function() { return {x: player.x, y: player.y - 1}; },
    Right: function() { return {x: player.x + 1, y: player.y}; },
    Down: function() { return {x: player.x, y: player.y + 1}; },
    Left: function() { return {x: player.x - 1, y: player.y}; },
    _: function() { return player; }
  }, action);
};

Or with ECMAScript 6 syntax.

const advancePlayer = (action, player) =>
  Action.case({
    Up: () => ({x: player.x, y: player.y - 1}),
    Right: () => ({x: player.x + 1, y: player.y}),
    Down: () => ({x: player.x, y: player.y + 1}),
    Left: () => ({x: player.x - 1, y: player.y}),
    _: () => player,
  }, action);

case will extract the fields of a value and pass them in order to the relevant function. A function to calculate the area of a shape could, for instance, look like this.

var Shape = Type({Circle: [Number, Point],
                  Rectangle: [Point, Point]});
var area = (shape) =>
  Shape.case({
    Circle: (radius, _) => Math.PI * radius * radius,
    Rectangle: (p1, p2) => (p2[0] - p1[0]) * (p2[1] - p1[1])
  }, shape);

case is curried so we could have created the above function simply by not passing the second parameter to case.

var area = Shape.case({
  Circle: (radius, _) => Math.PI * radius * radius,
  Rectangle: (p1, p2) => (p2[0] - p1[0]) * (p2[1] - p1[1])
});

caseOn is similar to case, but allows passing additional data directly into each case function. With caseOn, the advancePlayer example from before could be written in "point-free style" like this:

// No need to wrap this into a function that passes `player`
const advancePlayer = Action.caseOn({
  Up: (player) => ({x: player.x, y: player.y - 1}),
  Right: (player) => ({x: player.x + 1, y: player.y}),
  Down: (player) => ({x: player.x, y: player.y + 1}),
  Left: (player) => ({x: player.x - 1, y: player.y}),
  _: (player) => player
});

advancePlayer(Action.Up, player);

As a catch all you can supply a property with the key _ to case. When a type doesn't match another handler _ will be used. The fields will NOT be extracted when matching on _ as this may result in inconsistent argument positions.

const advancePlayerOnlyUp = (action, player) =>
  Action.case({
    Up: () => ({x: player.x, y: player.y - 1}),
    _: () => player,
  });

In addition to the static case and caseOn functions on a type, instances of a type have case and caseOf methods, so for example

Action.case({
  Up: () => ({x: player.x, y: player.y - 1}),
  Right: () => ({x: player.x + 1, y: player.y}),
  Down: () => ({x: player.x, y: player.y + 1}),
  Left: () => ({x: player.x - 1, y: player.y}),
  _: () => player,
}, action);

could equivalently be written as

action.case({
  Up: () => ({x: player.x, y: player.y - 1}),
  Right: () => ({x: player.x + 1, y: player.y}),
  Down: () => ({x: player.x, y: player.y + 1}),
  Left: () => ({x: player.x - 1, y: player.y}),
  _: () => player,
});

Extracting fields from a union type

If your type was defined using the record syntax you can access the fields through the name you specified:

var Person = Type({Person: {name: String, age: Number, shape: Shape}});
var person = Person.PersonOf({name: 'Simon', age: 21, shape: Circle});
var name = person.name;
var age = person.age;
var favoriteShape = person.shape;

If your type was not created using the record syntax the fields have to be extracted by indexing your union type:

var Person = Type({Person: [String, Number, Shape]});
var person = Person.Person('Simon', 21, Circle);
var name = person[0];
var age = person[1];
var favoriteShape = person[2];

Using the destructuring assignment in ECMAScript 6 it is possible to concisely extract all fields of a type.

var [name, age, favoriteShape] = person;

Recursive union types

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'

Disabling type checking

Type checking can be disabled, for instance in production, by setting Type.check to false.

Author & license

union-type was made by paldepind and is released under the MIT license. I hope you find it useful.

More Repositories

1

flyd

The minimalistic but powerful, modular, functional reactive programming library in JavaScript.
JavaScript
1,563
star
2

functional-frontend-architecture

A functional frontend framework.
JavaScript
1,443
star
3

synceddb

Makes it easy to write offline-first applications with realtime syncing and server side persistence.
JavaScript
404
star
4

dffptch

A micro library for diffing and patching JSON objects using a compact diff format
JavaScript
172
star
5

composable.el

Composable text editing for Emacs.
Emacs Lisp
115
star
6

projectdo

Context-aware single-letter project commands to speed up your terminal workflow.
Shell
60
star
7

functionize

A library which aids in making any JavaScript library more functional.
JavaScript
49
star
8

Kran

An entity system written in JavaScript.
JavaScript
42
star
9

Gtk98Icons

An icon theme for GTK that looks like Windows 98
PHP
40
star
10

smart-comment

Smarter commenting for Emacs
Emacs Lisp
40
star
11

sync-promise

Compact synchronized promise implementation. Promises/A+ incompliant. Works inside IdexedDB transactions.
JavaScript
32
star
12

dot-compose

Function composition with dot as a composition operator.
JavaScript
23
star
13

list-difference

Fast algorithm for finding edits between lists.
JavaScript
11
star
14

seamless-fantasy

Make fantasy land seamlessly compatible with plain JavaScript data structures.
JavaScript
10
star
15

duck

πŸ¦† Turns a TypeScript file into JSON describing the files exports.
TypeScript
8
star
16

find-the-function

A tiny tool for finding functions from libraries
TypeScript
7
star
17

ryter

A tiny JavaScript router
JavaScript
7
star
18

web-swipe-view

Horizontal swipe views for mobile web applications
JavaScript
7
star
19

finger-tree

Highly optimized implementation of finger trees in JavaScript
TypeScript
6
star
20

flyview

Efficient views powered by streams/ovservables/functional reactive properties.
JavaScript
6
star
21

fake-raf

A fake requestAnimationFrame perfect for unit testing.
JavaScript
5
star
22

maxima-calculus2

Maxima funktioner til lΓΈsning af eksamensopgaver i kurset Calculus 2 pΓ₯ Aarhus Universitet
4
star
23

flyd-forwardto

Create a new stream that passes all values through a function and forwards them to a target stream.
JavaScript
4
star
24

turing-patterns

Multi-Scale Turing Patterns
JavaScript
4
star
25

reflex-examples

A collection of examples using Reflex.
Haskell
3
star
26

flyd-obj

Functions for working with stream in objects.
JavaScript
3
star
27

flyd-scanmerge

Flyd module for conveniently merging and reducing several streams into one.
JavaScript
3
star
28

category-theory-notes

TeX
3
star
29

matmod

Handy functions for matMod written in R for Jupyter
Jupyter Notebook
3
star
30

dnd-scroll

Proper edge scroll when dragging with HTML 5 drag and drop!
JavaScript
3
star
31

dffptch-haskell

A small library for diffing and patching JSON objects using a compact diff format
Haskell
2
star
32

keyano-vscode

Next-generation keyboard-driven editing language. Edit code at the speed of light.
TypeScript
2
star
33

flyd-filter

Filter function for Flyd.
JavaScript
2
star
34

planetsimulator

A physical simulation of planetary motion written in JavaScript
JavaScript
2
star
35

paldepind.github.io

CSS
1
star
36

hareactive-old

Experimental WIP.
JavaScript
1
star
37

flyd-every

Takes a time interval t and creates a stream of the current time updated every t.
JavaScript
1
star
38

vdom-benchmark-snabbdom

Virtual DOM Benchmark implementation for Snabbdom library.
JavaScript
1
star
39

simple-frp

An attempt at creating a very simple FRP library for educational purposes.
TypeScript
1
star
40

flyd-sampleon

sampleOn for Flyd.
JavaScript
1
star
41

flyd-keepwhen

keepWhen function for Flyd.
JavaScript
1
star
42

software-foundations

My solutions to exercises in Benjamin C. Pierce's Software Foundations
HTML
1
star
43

flyd-lift

Lift function for Flyd.
JavaScript
1
star
44

react-native-chainable-stylesheet

TypeScript
1
star
45

dotfiles

Repository containing my dotfiles.
Emacs Lisp
1
star
46

flyd-aftersilence

Flyd module that buffers values from a stream and emits them after a specified duration of silience.
JavaScript
1
star
47

domain-theory

CSS
1
star