static-tagged-union
v1.0.1
Published
Static Tagged Union
Downloads
732
Maintainers
Readme
static-tagged-union
A simple, minimal, flexible, forgiving, static, serializable, tagged union utility.
Definition
Tagged unions, also known as sum types or union types, are useful for handling cases in a way
that is nicer than switch/case
or if/else
.
Aside from tagged unions having cases that you define, there are specific unions that are commonly useful:
- Maybe (Nothing, Just)
- Either (Left, Right)
- Result (Ok, Err)
These types are similar and, in static-tagged-union
, are combined into a single helper type:
- Maybe (N, Y)
Just like general TaggedUnion
s, Maybe
instances can optionally contain params.
That goes for both N
and Y
. In that sense, Maybe
is closer to Either
and Result
.
Maybe
is provided for convenience, since it is very common to have present/absent values,
true/false values, and other binary possibilities.
Motivation
I looked at several similar libraries, really trying to find one that meets all my requirements, namely:
- static functions to handle cases, not instance functions on returned types
- serializable representation: JSON.parse(JSON.stringify(instance)) must work
- non-exhaustive case handling: although exhaustive case handling can be seen as an advantage, because the library prevents you from forgetting to handle a case, I actually prefer to be able to handle only some cases and ignore the others
- default case handling: be able to define a handler that gets called if a case does not match any of the handlers that you have defined.
All of the similar libraries that I looked at matched some, but not all, of those requirements:
I give credit to the libraries above. They are excellent work and definitely do more robust verifications of your code than what I provide here.
Although the initial version of static-tagged-union
did no verifications, the current version
provides optional checking that:
- the case handlers match the case type (see
foldChecked
, below) - the case handlers match all the cases of the case type (see
foldStrict
, below)
Installation
Using npm
:
npm i static-tagged-union
Using a script
tag:
<script src="https://unpkg.com/static-tagged-union"></script>
Using the script
tag exposes a TaggedUnion
global, under which TaggedUnion
, fold
, etc.
are provided.
API
TaggedUnion(listOfCases)
fold(handlerObject)(caseInstance)
cases(listOfCases)(handlerFunction)
TaggedUnionChecked(type, listOfCases)
foldChecked(handlerObject)(checkedCaseInstance)
foldStrict(handlerObject)(checkedCaseInstance)
That is the core API. For convenience, there is also Maybe
:
Maybe.N(params)
,Maybe.Y(params)
bifold(fn, fy)
-- shortcut forfold({ N: fn, Y: fy })
map(fn)(maybeInstance)
- if
maybeInstance
isN
, returnsmaybeInstance
- if
maybeInstance
isY
, returnsMaybe.Y(fn(maybeInstance.params))
- if
unless(fn)(maybeInstance)
- if
maybeInstance
isN
, returnsMaybe.N(fn(maybeInstance.params))
- if
maybeInstance
isY
, returnsmaybeInstance
- if
bimap(fn, fy)
: shortcut for
fold({
N: params => Maybe.N(fn(params)),
Y: params => Maybe.Y(fy(params))
})
contains(caseInstance)([caseInstance0, caseInstance1, ...])
- if one of the caseInstances in the array is the same case as
caseInstance
, returnsMaybe.Y(params)
, whereparams
is the params contained by the first match - otherwise, returns
Maybe.N()
- if one of the caseInstances in the array is the same case as
Usage
Define a tagged union:
const Route = TaggedUnion(["Home", "Profile", "Login"])
The single argument to TaggedUnion
is an array of strings defining the cases for the type.
These will be properties of the returned object (Route
in this example), and so the strings
must be valid to be used as object properties.
Create instances of the tagged union:
const route1 = Route.Home()
const route2 = Route.Profile({ id: 42 })
For each case that you provided when creating the tagged union, you get a function that creates
an instance. The function accepts a single optional argument that becomes the params stored in
the instance. Each instance is a plain JavaScript object of the form { id: ..., params: ... }
Use fold
to handle cases:
fold({
Home: () => "Home",
Profile: ({ id }) => `Profile ${id}`
})(route2)
// Returns "Profile 42"
You can provide just the cases you want to handle. If there is no match, the call returns
undefined
:
fold({
Home: () => "Home"
})(route2)
// Returns undefined
You can also use _
to indicate a default handler that gets called when there is no match:
fold({
Home: () => "Home",
_: () => "No match was found"
})(route2)
// Returns "No match was found"
If you want to handle multiple cases in the same way, use cases
for convenience:
fold(cases(["Home", "About"])(() => "Result"))
fold({
...cases(["Home", "About"])(() => "Result"),
User: () => "User",
_: () => "No match found"
})
Instead of {...cases()}
, you can also pass multiple handler objects to fold
:
fold(
cases(["Home", "About"])(() => "Result"),
{ User: () => "User",
_: () => "No match found"
}
)
Credit: thank you James Forbes for this idea!
TaggedUnionChecked, foldChecked, and foldStrict
The API so far does not do any verifications on cases and handlers. Optionally, you can ensure that:
- the case handlers match the case type (see
foldChecked
, below) - the case handlers match all the cases of the case type (see
foldStrict
, below)
First, use TaggedUnionChecked
to create the tagged union:
const Route = TaggedUnionChecked(["Home", "Profile", "Login"])
Next, use foldChecked
to make sure that your handlers match the case type:
// This works fine:
foldChecked({
Profile: ({ id }) => `Profile ${id}`
})(Route.Profile({ id: 42 }))
// This throws an error because `User` is not part of the cases for `Route`:
foldChecked({
User: ({ id }) => `User ${id}`
})(Route.Profile({ id: 42 }))
Finally, use foldStrict
to make the same verifications as foldChecked
plus requiring that
the handler handles all the cases of the type. Note that _
cannot be used here; instead, use
cases
to handle multiple cases.
// This works fine:
foldStrict({
Home: () => "Home",
Profile: ({ id }) => `Profile ${id}`,
Login: () => "Login"
})(Route.Profile({ id: 42 }))
// This also works fine:
foldStrict(
cases(["Home", "Login"])(() => "Page"),
{ Profile: ({ id }) => `Profile ${id}` }
)(Route.Profile({ id: 42 }))
// This throws an error because `Login` is not handled:
foldStrict({
Home: () => "Home",
Profile: ({ id }) => `Profile ${id}`
})(Route.Profile({ id: 42 }))
Use Maybe
for convenience:
Whenever you have a value that can be true/false, present/absent, and so on, you can use
Maybe.Y
and Maybe.N
:
let data = Maybe.N()
// later...
data = Maybe.Y({ things: ... })
If you like, you can define an alias:
const Loaded = Maybe
let data = Loaded.N()
// later...
data = Loaded.Y({ things: ... })
As a shortcut to fold({ N: () => ..., Y: params => ... })
, you can use bifold
:
bifold(
() => "Loading, please wait...",
({ things }) => `Here are the things: ${things}`
)
Note that N
accepts params as well. For example, you might use this for an error message.
Maybe
can also be used to denote the presence or absence of a value, and provide a convenient
way to operate on a value only if it is present. Use map
for this:
const name = getName() // returns Maybe.N or Maybe.Y
const upper = map(str => str.toUpperCase())(name) // only if name is present
bifold(
() => "No name given.",
name => `Hello, ${name}` // Hello, FRED
)(upper)
Finally, you can do something with the params in N
and something with the params in Y
with bimap
:
const name = getName() // returns Maybe.N or Maybe.Y
const converted = bimap(
err => `Error message: ${err}`,
name => name.toUpperCase()
)
bifold(
msg => console.err(msg),
name => `Hello, ${name}`
)(converted)
Note that Maybe
, map
, bimap
, and bifold
are all just convenience shortcuts. You can
achieve the same with just TaggedUnion
and fold
.
Credits
Special thanks to James Forbes for static-sum-type and stags. Definitely check those out if you want static tagged unions with verifications for types and exhaustive case handling.
The readme for stags also gives a great
explanation of why the Maybe
type is so useful.
static-tagged-union is developed by foxdonut (@foxdonut00) and is released under the MIT license.