This is the standard correct definition for a function which is a proper
subtype of all other functions: parameters of a function subtype must be
wider* than those of the base type, and return types must be narrower.
Everything is wider than never[]
and narrower than unknown
, so any
function is assignable to places this is used.
A lightweight object defining how to handle each variant of a Maybe
.
You can use the discriminant via the variant
property of Maybe
¿
instances if you need to match explicitly on it.
Legacy alias for transposeArray
.
You can think of this like a short-circuiting logical "and" operation on a
Maybe
type. If maybe
is just
, then the result is the
andMaybe
. If maybe
is Nothing
, the result is Nothing
.
This is useful when you have another Maybe
value you want to provide if and
only if* you have a Just
– that is, when you need to make sure that if you
Nothing
, whatever else you're handing a Maybe
to also gets a Nothing
.
Notice that, unlike in map
or its variants, the original maybe
is
not involved in constructing the new Maybe
.
import Maybe from 'true-myth/maybe';
const justA = Maybe.just('A');
const justB = Maybe.just('B');
const nothing: Maybe<number> = nothing();
console.log(Maybe.and(justB, justA).toString()); // Just(B)
console.log(Maybe.and(justB, nothing).toString()); // Nothing
console.log(Maybe.and(nothing, justA).toString()); // Nothing
console.log(Maybe.and(nothing, nothing).toString()); // Nothing
The type of the initial wrapped value.
The type of the wrapped value of the returned Maybe
.
The Maybe
instance to return if maybe
is Just
The Maybe
instance to check.
Nothing
if the original maybe
is Nothing
, or andMaybe
if the original maybe
is Just
.
Apply a function to the wrapped value if Just
and return a new
Just
containing the resulting value; or return Nothing
if
Nothing
.
This differs from map
in that thenFn
returns another Maybe
.
You can use andThen
to combine two functions which both create a Maybe
from an unwrapped type.
You may find the .then
method on an ES6 Promise
helpful for comparison: if
you have a Promise
, you can pass its then
method a callback which returns
another Promise
, and the result will not be a nested promise, but a single
Promise
. The difference is that Promise#then
unwraps all layers to only
ever return a single Promise
value, whereas Maybe.andThen
will not unwrap
nested Maybe
s.
This is sometimes also known as bind
, but not aliased as such because
bind
already means something in JavaScript.
(This is a somewhat contrived example, but it serves to show the way the function behaves.)
import Maybe, { andThen, toString } from 'true-myth/maybe';
// string -> Maybe<number>
const toMaybeLength = (s: string) => Maybe.of(s.length);
// Maybe<string>
const aMaybeString = Maybe.of('Hello, there!');
// Maybe<number>
const resultingLength = andThen(toMaybeLength, aMaybeString);
console.log(toString(resultingLength)); // 13
Note that the result is not Just(Just(13))
, but Just(13)
!
The type of the wrapped value.
The type of the wrapped value in the resulting Maybe
.
The function to apply to the wrapped T
if maybe
is Just
.
The Maybe
to evaluate and possibly apply a function to the
contents of.
The result of the thenFn
(a new Maybe
) if maybe
is a
Just
, otherwise Nothing
if maybe
is a Nothing
.
Allows you to apply (thus ap
) a value to a function without having to take
either out of the context of their Maybe
s. This does mean that the
transforming function is itself within a Maybe
, which can be hard to grok at
first but lets you do some very elegant things. For example, ap
allows you
to this:
import { just, nothing } from 'true-myth/maybe';
const one = just(1);
const five = just(5);
const none = nothing();
const add = (a: number) => (b: number) => a + b;
const maybeAdd = just(add);
maybeAdd.ap(one).ap(five); // Just(6)
maybeAdd.ap(one).ap(none); // Nothing
maybeAdd.ap(none).ap(five) // Nothing
Without ap
, you'd need to do something like a nested match
:
import { just, nothing } from 'true-myth/maybe';
const one = just(1);
const five = just(5);
const none = nothing();
one.match({
Just: n => five.match({
Just: o => just(n + o),
Nothing: () => nothing(),
}),
Nothing: () => nothing(),
}); // Just(6)
one.match({
Just: n => none.match({
Just: o => just(n + o),
Nothing: () => nothing(),
}),
Nothing: () => nothing(),
}); // Nothing
none.match({
Just: n => five.match({
Just: o => just(n + o),
Nothing: () => nothing(),
}),
Nothing: () => nothing(),
}); // Nothing
And this kind of thing comes up quite often once you're using Maybe
to
handle optionality throughout your application.
For another example, imagine you need to compare the equality of two
ImmutableJS data structures, where a ===
comparison won't work. With ap
,
that's as simple as this:
import Maybe from 'true-myth/maybe';
import { is as immutableIs, Set } from 'immutable';
const is = (first: unknown) => (second: unknown) =>
immutableIs(first, second);
const x = Maybe.of(Set.of(1, 2, 3));
const y = Maybe.of(Set.of(2, 3, 4));
Maybe.of(is).ap(x).ap(y); // Just(false)
Without ap
, we're back to that gnarly nested match
:
import Maybe, { just, nothing } from 'true-myth/maybe';
import { is, Set } from 'immutable';
const x = Maybe.of(Set.of(1, 2, 3));
const y = Maybe.of(Set.of(2, 3, 4));
x.match({
Just: iX => y.match({
Just: iY => Maybe.just(is(iX, iY)),
Nothing: () => Maybe.nothing(),
})
Nothing: () => Maybe.nothing(),
}); // Just(false)
In summary: anywhere you have two Maybe
instances and need to perform an
operation that uses both of them, ap
is your friend.
Two things to note, both regarding currying:
ap
must be curried. That is, they must be of the
form (for add) (a: number) => (b: number) => a + b
, not the more usual
(a: number, b: number) => a + b
you see in JavaScript more generally.(Unfortunately, these do not currently work with lodash or Ramda's curry
helper functions. A future update to the type definitions may make that
work, but the intermediate types produced by those helpers and the more
general function types expected by this function do not currently align.)
ap
as many times as there are arguments to the
function you're dealing with. So in the case of this add3
function,
which has the "arity" (function argument count) of 3 (a
and b
), you'll
need to call ap
twice: once for a
, and once for b
. To see why, let's
look at what the result in each phase is:const add3 = (a: number) => (b: number) => (c: number) => a + b + c;
const maybeAdd = just(add); // Just((a: number) => (b: number) => (c: number) => a + b + c)
const maybeAdd1 = maybeAdd.ap(just(1)); // Just((b: number) => (c: number) => 1 + b + c)
const maybeAdd1And2 = maybeAdd1.ap(just(2)) // Just((c: number) => 1 + 2 + c)
const final = maybeAdd1.ap(just(3)); // Just(4)
So for toString
, which just takes a single argument, you would only need
to call ap
once.
const toStr = (v: { toString(): string }) => v.toString();
just(toStr).ap(12); // Just("12")
One other scenario which doesn't come up quite as often but is conceivable
is where you have something that may or may not actually construct a function
for handling a specific Maybe
scenario. In that case, you can wrap the
possibly-present in ap
and then wrap the values to apply to the function to
in Maybe
themselves.
Aside: ap
is not named apply
because of the overlap with JavaScript's
existing apply
function – and although strictly speaking, there isn't any
direct overlap (Maybe.apply
and Function.prototype.apply
don't intersect
at all) it's useful to have a different name to avoid implying that they're
the same.
Allows quick triple-equal equality check between the values inside two
maybe
instances without having to unwrap them first.
const a = Maybe.of(3);
const b = Maybe.of(3);
const c = Maybe.of(null);
const d = Maybe.nothing();
Maybe.equals(a, b); // true
Maybe.equals(a, c); // false
Maybe.equals(c, d); // true
Safely search for an element in an array.
This function behaves like Array.prototype.find
, but returns Maybe<T>
instead of T | undefined
.
The basic form is:
import Maybe from 'true-myth/maybe';
let array = [1, 2, 3];
Maybe.find(v => v > 1, array); // Just(2)
Maybe.find(v => v < 1, array); // Nothing
The function is curried so you can use it in a functional chain. For example
(leaving aside error handling on a bad response for simplicity), suppose the
url https://arrays.example.com
returned a JSON payload with the type
Array<{ count: number, name: string }>
, and we wanted to get the first
of these where count
was at least 100. We could write this:
import Maybe from 'true-myth/maybe';
type Item = { count: number; name: string };
type Response = Array<Item>;
// curried variant!
const findAtLeast100 = Maybe.find(({ count }: Item) => count > 100);
fetch('https://arrays.example.com')
.then(response => response.json() as Response)
.then(findAtLeast100)
.then(found => {
if (found.isJust) {
console.log(`The matching value is ${found.value.name}!`);
}
});
A function to execute on each value in the array, returning
true
when the item in the array matches the condition. The
signature for predicate
is identical to the signature for
the first argument to Array.prototype.find
. The function
is called once for each element of the array, in ascending
order, until it finds one where predicate returns true. If
such an element is found, find immediately returns that
element value wrapped in Just
. Otherwise, Maybe.find
returns Nothing
.
The array to search using the predicate.
Safely get the first item from a list, returning Just
the first
item if the array has at least one item in it, or Nothing
if it is
empty.
let empty = [];
Maybe.head(empty); // => Nothing
let full = [1, 2, 3];
Maybe.head(full); // => Just(1)
The array to get the first item from.
Safely extract a key from a Maybe
of an object, returning
Just
if the key has a value on the object and
{@linkcode MaybNothing} if it does not. (Like property
but
operating on a Maybe<T>
rather than directly on a T
.)
The check is type-safe: you won't even be able to compile if you try to look up a property that TypeScript knows doesn't exist on the object.
import { get, just, nothing } from 'true-myth/maybe';
type Person = { name?: string };
const me: Maybe<Person> = just({ name: 'Chris' });
console.log(get('name', me)); // Just('Chris')
const nobody = nothing<Person>();
console.log(get('name', nobody)); // Nothing
However, it also works correctly with dictionary types:
import { get, just } from 'true-myth/maybe';
type Dict<T> = { [key: string]: T };
const score: Maybe<Dict<number>> = just({
player1: 0,
player2: 1
});
console.log(get('player1', score)); // Just(0)
console.log(get('player2', score)); // Just(1)
console.log(get('player3', score)); // Nothing
The order of keys is so that it can be partially applied:
import { get, just } from 'true-myth/maybe';
type Person = { name?: string };
const lookupName = get('name');
const me: Person = { name: 'Chris' };
console.log(lookupName(me)); // Just('Chris')
const nobody: Person = {};
console.log(lookupName(nobody)); // Nothing
The key to pull out of the object.
A convenience alias for Maybe.first
.
null
and undefined
are allowed by the type signature so that the
function may throw
on those rather than constructing a type like
Maybe<undefined>
.
The type of the item contained in the Maybe
.
The value to wrap in a Maybe.Just
.
An instance of Maybe.Just<T>
.
Safely get the last item from a list, returning Just
the last item
if the array has at least one item in it, or Nothing
if it is
empty.
let empty = [];
Maybe.last(empty); // => Nothing
let full = [1, 2, 3];
Maybe.last(full); // => Just(3)
The array to get the first item from.
Map over a Maybe
instance: apply the function to the wrapped value
if the instance is Just
, and return Nothing
if the
instance is Nothing
.
map
works a lot like Array.prototype.map
: Maybe
and Array
are both
containers* for other things. If you have no items in an array of numbers
named foo
and call foo.map(x => x + 1)
, you'll still just have an array
with nothing in it. But if you have any items in the array ([2, 3]
), and you
call foo.map(x => x + 1)
on it, you'll get a new array with each of those
items inside the array "container" transformed ([3, 4]
).
That's exactly what's happening with map
. If the container is empty – the
Nothing
variant – you just get back an empty container. If the container has
something in it – the Just
variant – you get back a container with the item
inside transformed.
(So... why not just use an array? The biggest reason is that an array can be
any length. With a Maybe
, we're capturing the idea of "something or nothing"
rather than "0 to n" items. And this lets us implement a whole set of other
interfaces, like those in this module.)
const length = (s: string) => s.length;
const justAString = Maybe.just('string');
const justTheStringLength = map(length, justAString);
console.log(justTheStringLength.toString()); // Just(6)
const notAString = Maybe.nothing<string>();
const notAStringLength = map(length, notAString);
console.log(notAStringLength.toString()); // "Nothing"
The type of the wrapped value.
The type of the wrapped value of the returned Maybe
.
The function to apply the value to if Maybe
is Just
.
A new Maybe
with the result of applying mapFn
to the value in
a Just
, or Nothing
if maybe
is Nothing
.
Map over a Maybe
instance: apply the function to the wrapped value
if the instance is Just
, and return Nothing
if the
instance is Nothing
.
map
works a lot like Array.prototype.map
: Maybe
and Array
are both
containers* for other things. If you have no items in an array of numbers
named foo
and call foo.map(x => x + 1)
, you'll still just have an array
with nothing in it. But if you have any items in the array ([2, 3]
), and you
call foo.map(x => x + 1)
on it, you'll get a new array with each of those
items inside the array "container" transformed ([3, 4]
).
That's exactly what's happening with map
. If the container is empty – the
Nothing
variant – you just get back an empty container. If the container has
something in it – the Just
variant – you get back a container with the item
inside transformed.
(So... why not just use an array? The biggest reason is that an array can be
any length. With a Maybe
, we're capturing the idea of "something or nothing"
rather than "0 to n" items. And this lets us implement a whole set of other
interfaces, like those in this module.)
const length = (s: string) => s.length;
const justAString = Maybe.just('string');
const justTheStringLength = map(length, justAString);
console.log(justTheStringLength.toString()); // Just(6)
const notAString = Maybe.nothing<string>();
const notAStringLength = map(length, notAString);
console.log(notAStringLength.toString()); // "Nothing"
The Maybe
instance to map over.
A new Maybe
with the result of applying mapFn
to the value in
a Just
, or Nothing
if maybe
is Nothing
.
Map over a Maybe
instance and get out the value if maybe
is a
Just
, or return a default value if maybe
is a
Nothing
.
const length = (s: string) => s.length;
const justAString = Maybe.just('string');
const theStringLength = mapOr(0, length, justAString);
console.log(theStringLength); // 6
const notAString = Maybe.nothing<string>();
const notAStringLength = mapOr(0, length, notAString)
console.log(notAStringLength); // 0
The type of the wrapped value.
The type of the wrapped value of the returned Maybe
.
The default value to use if maybe
is Nothing
The function to apply the value to if Maybe
is Just
The Maybe
instance to map over.
Map over a Maybe
instance and get out the value if maybe
is a
Just
, or use a function to construct a default value if maybe
is
Nothing
.
const length = (s: string) => s.length;
const getDefault = () => 0;
const justAString = Maybe.just('string');
const theStringLength = mapOrElse(getDefault, length, justAString);
console.log(theStringLength); // 6
const notAString = Maybe.nothing<string>();
const notAStringLength = mapOrElse(getDefault, length, notAString)
console.log(notAStringLength); // 0
The type of the wrapped value.
The type of the wrapped value of the returned Maybe
.
The function to apply if maybe
is Nothing
.
The function to apply to the wrapped value if maybe
is
Just
The Maybe
instance to map over.
Performs the same basic functionality as unwrapOrElse
, but instead
of simply unwrapping the value if it is Just
and applying a value
to generate the same default type if it is Nothing
, lets you
supply functions which may transform the wrapped type if it is Just
or get a
default value for Nothing
.
This is kind of like a poor man's version of pattern matching, which JavaScript currently lacks.
Instead of code like this:
import Maybe from 'true-myth/maybe';
const logValue = (mightBeANumber: Maybe<number>) => {
const valueToLog = Maybe.mightBeANumber.isJust
? mightBeANumber.value.toString()
: 'Nothing to log.';
console.log(valueToLog);
};
...we can write code like this:
import { match } from 'true-myth/maybe';
const logValue = (mightBeANumber: Maybe<number>) => {
const value = match(
{
Just: n => n.toString(),
Nothing: () => 'Nothing to log.',
},
mightBeANumber
);
console.log(value);
};
This is slightly longer to write, but clearer: the more complex the resulting expression, the hairer it is to understand the ternary. Thus, this is especially convenient for times when there is a complex result, e.g. when rendering part of a React component inline in JSX/TSX.
A lightweight object defining what to do in the case of each variant.
The maybe
instance to check.
If you want to create an instance with a specific type, e.g. for use in a
function which expects a Maybe<T>
where the <T>
is known but you have no
value to give it, you can use a type parameter:
const notString = Maybe.nothing<string>();
The type of the item contained in the Maybe
.
An instance of Maybe.Nothing<T>
.
Create a Maybe
from any value.
To specify that the result should be interpreted as a specific type, you may
invoke Maybe.of
with an explicit type parameter:
import * as Maybe from 'true-myth/maybe';
const foo = Maybe.of<string>(null);
This is usually only important in two cases:
Nothing
from a known null
or
undefined value which is untyped.The type of the item contained in the Maybe
.
The value to wrap in a Maybe
. If it is undefined
or null
,
the result will be Nothing
; otherwise it will be the type of
the value passed.
Provide a fallback for a given Maybe
. Behaves like a logical or
:
if the maybe
value is a Just
, returns that maybe
; otherwise,
returns the defaultMaybe
value.
This is useful when you want to make sure that something which takes a Maybe
always ends up getting a Just
variant, by supplying a default value for the
case that you currently have a nothing.
import Maybe from 'true-utils/maybe';
const justA = Maybe.just("a");
const justB = Maybe.just("b");
const aNothing: Maybe<string> = nothing();
console.log(Maybe.or(justB, justA).toString()); // Just(A)
console.log(Maybe.or(aNothing, justA).toString()); // Just(A)
console.log(Maybe.or(justB, aNothing).toString()); // Just(B)
console.log(Maybe.or(aNothing, aNothing).toString()); // Nothing
The type of the wrapped value.
The Maybe
to use if maybe
is a Nothing
.
The Maybe
instance to evaluate.
maybe
if it is a Just
, otherwise defaultMaybe
.
Sometimes you need to perform an operation using other data in the environment
to construct the fallback value. In these situations, you can pass a function
(which may be a closure) as the elseFn
to generate the fallback Maybe<T>
.
Useful for transforming empty scenarios based on values in context.
The type of the wrapped value.
The function to apply if maybe
is Nothing
The maybe
to use if it is Just
.
The maybe
if it is Just
, or the Maybe
returned by elseFn
if the maybe
is Nothing
.
Safely extract a key from an object, returning Just
if the key has
a value on the object and Nothing
if it does not.
The check is type-safe: you won't even be able to compile if you try to look up a property that TypeScript knows doesn't exist on the object.
type Person = { name?: string };
const me: Person = { name: 'Chris' };
console.log(Maybe.property('name', me)); // Just('Chris')
const nobody: Person = {};
console.log(Maybe.property('name', nobody)); // Nothing
However, it also works correctly with dictionary types:
type Dict<T> = { [key: string]: T };
const score: Dict<number> = {
player1: 0,
player2: 1
};
console.log(Maybe.property('player1', score)); // Just(0)
console.log(Maybe.property('player2', score)); // Just(1)
console.log(Maybe.property('player3', score)); // Nothing
The order of keys is so that it can be partially applied:
type Person = { name?: string };
const lookupName = Maybe.property('name');
const me: Person = { name: 'Chris' };
console.log(lookupName(me)); // Just('Chris')
const nobody: Person = {};
console.log(lookupName(nobody)); // Nothing
The key to pull out of the object.
The object to look up the key from.
Local implementation of {@linkcode Toolbelt.toOkOrElseErr toOkOrElseErr} from {@linkcode Toolbelt} for backwards compatibility. Prefer to import it from there instead:
import type { toOkOrElseErr } from 'true-myth/toolbelt';
Local implementation of {@linkcode Toolbelt.toOkOrErr toOkOrErr} from {@linkcode Toolbelt} for backwards compatibility. Prefer to import it from there instead:
import type { toOkOrErr } from 'true-myth/toolbelt';
Create a String
representation of a Maybe
instance.
A Just
instance will be Just(<representation of the value>)
,
where the representation of the value is simply the value's own toString
representation. For example:
call | output |
---|---|
toString(Maybe.of(42)) |
Just(42) |
toString(Maybe.of([1, 2, 3])) |
Just(1,2,3) |
toString(Maybe.of({ an: 'object' })) |
Just([object Object]) |
toString(Maybe.nothing()) |
Nothing |
The type of the wrapped value; its own .toString
will be used
to print the interior contents of the Just
variant.
The value to convert to a string.
The string representation of the Maybe
.
Given an array or tuple of Maybe
s, return a Maybe
of the array
or tuple values.
Array<Maybe<A> | Maybe<B>>
, the resulting type is
Maybe<Array<A | B>>
.[Maybe<A>, Maybe<B>]
, the resulting type is
Maybe<[A, B]>
.If any of the items in the array or tuple are Nothing
, the whole
result is Nothing
. If all items in the array or tuple are Just
,
the whole result is Just
.
Given an array with a mix of Maybe
types in it, both allJust
and mixed
here will have the type Maybe<Array<string | number>>
, but will be Just
and Nothing
respectively.
import Maybe from 'true-myth/maybe';
let valid = [Maybe.just(2), Maybe.just('three')];
let allJust = Maybe.arrayTranspose(valid); // => Just([2, 'three']);
let invalid = [Maybe.just(2), Maybe.nothing<string>()];
let mixed = Maybe.arrayTranspose(invalid); // => Nothing
When working with a tuple type, the structure of the tuple is preserved. Here,
for example, result
has the type Maybe<[string, number]>
and will be
Nothing
:
import Maybe from 'true-myth/maybe';
type Tuple = [Maybe<string>, Maybe<number>];
let invalid: Tuple = [Maybe.just('wat'), Maybe.nothing()];
let result = Maybe.arrayTranspose(invalid); // => Nothing
If all of the items in the tuple are Just
, the result is Just
wrapping the
tuple of the values of the items. Here, for example, result
again has the
type Maybe<[string, number]>
and will be Just(['hey', 12]
:
import Maybe from 'true-myth/maybe';
type Tuple = [Maybe<string>, Maybe<number>];
let valid: Tuple = [Maybe.just('hey'), Maybe.just(12)];
let result = Maybe.arrayTranspose(valid); // => Just(['hey', 12])
Note: this does not work with ReadonlyArray
. If you have a
ReadonlyArray
you wish to operate on, you must cast it to Array
insetad.
This cast is always safe here, because Array
is a wider type than
ReadonlyArray
.
The Maybe
s to resolve to a single Maybe
.
Legacy alias for transposeArray
.
Safely get the value out of a Maybe
.
Returns the content of a Just
or defaultValue
if
Nothing
. This is the recommended way to get a value out of a
Maybe
most of the time.
import Maybe from 'true-myth/maybe';
const notAString = Maybe.nothing<string>();
const isAString = Maybe.just('look ma! some characters!');
console.log(Maybe.unwrapOr('<empty>', notAString)); // "<empty>"
console.log(Maybe.unwrapOr('<empty>', isAString)); // "look ma! some characters!"
The type of the wrapped value.
The value to return if maybe
is a Nothing
.
The Maybe
instance to unwrap if it is a Just
.
The content of maybe
if it is a Just
, otherwise
defaultValue
.
Safely get the value out of a Maybe
by returning the wrapped value
if it is Just
, or by applying orElseFn
if it is
Nothing
.
This is useful when you need to generate a value (e.g. by using current
values in the environment – whether preloaded or by local closure) instead of
having a single default value available (as in unwrapOr
).
import Maybe from 'true-myth/maybe';
// You can imagine that someOtherValue might be dynamic.
const someOtherValue = 99;
const handleNothing = () => someOtherValue;
const aJust = Maybe.just(42);
console.log(Maybe.unwrapOrElse(handleNothing, aJust)); // 42
const aNothing = nothing<number>();
console.log(Maybe.unwrapOrElse(handleNothing, aNothing)); // 99
The wrapped value.
A function used to generate a valid value if maybe
is a
Nothing
.
The Maybe
instance to unwrap if it is a Just
Either the content of maybe
or the value returned from
orElseFn
.
Transform a function from a normal JS function which may return null
or
undefined
to a function which returns a Maybe
instead.
For example, dealing with the Document#querySelector
DOM API involves a
lot* of things which can be null
:
const foo = document.querySelector('#foo');
let width: number;
if (foo !== null) {
width = foo.getBoundingClientRect().width;
} else {
width = 0;
}
const getStyle = (el: HTMLElement, rule: string) => el.style[rule];
const bar = document.querySelector('.bar');
let color: string;
if (bar != null) {
let possibleColor = getStyle(bar, 'color');
if (possibleColor !== null) {
color = possibleColor;
} else {
color = 'black';
}
}
(Imagine in this example that there were more than two options: the
simplifying workarounds you commonly use to make this terser in JS, like the
ternary operator or the short-circuiting ||
or ??
operators, eventually
become very confusing with more complicated flows.)
We can work around this with Maybe
, always wrapping each layer in
{@linkcode Maybe.of} invocations, and this is somewhat better:
import Maybe from 'true-myth/maybe';
const aWidth = Maybe.of(document.querySelector('#foo'))
.map(el => el.getBoundingClientRect().width)
.unwrapOr(0);
const aColor = Maybe.of(document.querySelector('.bar'))
.andThen(el => Maybe.of(getStyle(el, 'color'))
.unwrapOr('black');
With wrapReturn
, though, you can create a transformed version of a function
once* and then be able to use it freely throughout your codebase, always
getting back a Maybe
:
import { wrapReturn } from 'true-myth/maybe';
const querySelector = wrapReturn(document.querySelector.bind(document));
const safelyGetStyle = wrapReturn(getStyle);
const aWidth = querySelector('#foo')
.map(el => el.getBoundingClientRect().width)
.unwrapOr(0);
const aColor = querySelector('.bar')
.andThen(el => safelyGetStyle(el, 'color'))
.unwrapOr('black');
The function to transform; the resulting function will have the exact same signature except for its return type.
Transform a function from a normal JS function which may return null
or
undefined
to a function which returns a Maybe
instead.
For example, dealing with the Document#querySelector
DOM API involves a
lot* of things which can be null
:
const foo = document.querySelector('#foo');
let width: number;
if (foo !== null) {
width = foo.getBoundingClientRect().width;
} else {
width = 0;
}
const getStyle = (el: HTMLElement, rule: string) => el.style[rule];
const bar = document.querySelector('.bar');
let color: string;
if (bar != null) {
let possibleColor = getStyle(bar, 'color');
if (possibleColor !== null) {
color = possibleColor;
} else {
color = 'black';
}
}
(Imagine in this example that there were more than two options: the
simplifying workarounds you commonly use to make this terser in JS, like the
ternary operator or the short-circuiting ||
or ??
operators, eventually
become very confusing with more complicated flows.)
We can work around this with Maybe
, always wrapping each layer in
{@linkcode Maybe.of} invocations, and this is somewhat better:
import Maybe from 'true-myth/maybe';
const aWidth = Maybe.of(document.querySelector('#foo'))
.map(el => el.getBoundingClientRect().width)
.unwrapOr(0);
const aColor = Maybe.of(document.querySelector('.bar'))
.andThen(el => Maybe.of(getStyle(el, 'color'))
.unwrapOr('black');
With wrapReturn
, though, you can create a transformed version of a function
once* and then be able to use it freely throughout your codebase, always
getting back a Maybe
:
import { wrapReturn } from 'true-myth/maybe';
const querySelector = wrapReturn(document.querySelector.bind(document));
const safelyGetStyle = wrapReturn(getStyle);
const aWidth = querySelector('#foo')
.map(el => el.getBoundingClientRect().width)
.unwrapOr(0);
const aColor = querySelector('.bar')
.andThen(el => safelyGetStyle(el, 'color'))
.unwrapOr('black');
Generated using TypeDoc
Maybe
A
Maybe<T>
represents a value of typeT
which may, or may not, be present.If the value is present, it is
Just(value)
. If it's absent, it'sNothing
. This provides a type-safe container for dealing with the possibility that there's nothing here – a container you can do many of the same things you might with an array – so that you can avoid nastynull
andundefined
checks throughout your codebase.The behavior of this type is checked by TypeScript at compile time, and bears no runtime overhead other than the very small cost of the container object and some lightweight wrap/unwrap functionality.
The
Nothing
variant has a type parameter<T>
so that type inference works correctly in TypeScript when operating onNothing
instances with functions which require aT
to behave properly, e.g.map
, which cannot check that the map function satisfies the type constraints forMaybe<T>
unlessNothing
has a parameterT
to constrain it on invocation.Put simply: without the type parameter, if you had a
Nothing
variant of aMaybe<string>
, and you tried to use it with a function which expected aMaybe<number>
it would still type check – because TypeScript doesn't have enough information to check that it doesn't meet the requirements.Using
Maybe
The library is designed to be used with a functional style, allowing you to compose operations easily. Thus, standalone pure function versions of every operation are supplied. However, the same operations also exist on the
Just
andNothing
types directly, so you may also write them in a more traditional "fluent" object style.Examples: functional style
Examples: fluent object invocation
Note: in the "class"-style, if you are constructing a
Maybe
from an unknown source, you must either do the work to check the value yourself, or useMaybe.of
– you can't know at that point whether it's safe to construct aJust
without checking, butof
will always work correctly!As you can see, it's often advantageous to use
Maybe.of
even if you're otherwise inclined to use the class method approach; it dramatically cuts down the boilerplate you have to write (since, under the hood, it's doing exactly this check!).Prefer
Maybe.of
In fact, if you're dealing with data you are not constructing directly yourself, always prefer to use [
Maybe.of
] to create a newMaybe
. If an API lies to you for some reason and hands you anundefined
or anull
(even though you expect it to be an actualT
in a specific scenario), the.of()
function will still construct it correctly for you.By contrast, if you do
Maybe.just(someVariable)
andsomeVariable
isnull
orundefined
, the program will throw at that point. This is a simple consequence of the need to make thenew Just()
constructor work; we cannot constructJust
safely in a way that excludes a type ofMaybe<null>
orMaybe<undefined>
otherwise – and that would defeat the whole purpose of using aMaybe
!Writing type constraints
Especially when constructing a
Nothing
, you may need to specify what kind ofNothing
it is. The TypeScript type system can figure it out based on the value passed in for aJust
, but there's no value to use with aNothing
, so you may have to specify it. In that case, you can write the type explicitly:Note that this is necessary if you declare the
Maybe
in a statement inside a function, but is not necessary when you have an expression-bodied arrow function or when you return the item directly:Using
Maybe
EffectivelyThe best times to create and safely unwrap
Maybe
s are at the boundaries of your application. When you are deserializing data from an API, or when you are handling user input, you can use aMaybe
instance to handle the possibility that there's just nothing there, if there is no good default value to use everywhere in the application. Within the business logic of your application, you can then safely deal with the data by using theMaybe
functions or method until you hit another boundary, and then you can choose how best to handle it at that point.You won't normally need to unwrap it at any point other than the boundaries, because you can simply apply any transformations using the helper functions or methods, and be confident that you'll have either correctly transformed data, or a
Nothing
, at the end, depending on your inputs.If you are handing data off to another API, for example, you might convert a
Nothing
right back into anull
in a JSON payload, as that's a reasonable way to send the data across the wire – the consumer can then decide how to handle it as is appropriate in its own context.If you are rendering a UI, having a
Nothing
when you need to render gives you an opportunity to provide default/fallback content – whether that's an explanation that there's nothing there, or a sensible substitute, or anything else that might be appropriate at that point in your app.You may also be tempted to use
Maybe
s to replace boolean flags or similar approaches for defining branching behavior or output from a function. You should avoid that, in general. ReserveMaybe
for modeling the possible absence of data, and nothing else. Prefer to useResult
orEither
for fallible or disjoint scenarios instead – or construct your own union types if you have more than two boundaries!Finally, and along similar lines, if you find yourself building a data structure with a lot of
Maybe
types in it, you should consider it a "code smell" – something wrong with your model. It usually means you should find a way to refactor the data structure into a union of smaller data structures which more accurately capture the domain you're modeling.