true-myth
Version:
A library for safe functional programming in JavaScript, with first-class support for TypeScript
572 lines (511 loc) • 18.9 kB
JavaScript
/**
A value of type `T` which may, or may not, be present. If the value is
present, it is {@linkcode Just Just(value)}. If it's absent, it is {@linkcode
Nothing} instead.
For a deep dive on the type, see [the guide](/guide/understanding/maybe.md).
@module
*/
import { curry1, isVoid, safeToString } from './-private/utils.js';
/**
Discriminant for the {@linkcode Just} and {@linkcode Nothing} type instances.
You can use the discriminant via the `variant` property of {@linkcode Maybe}
instances if you need to match explicitly on it.
*/
export const Variant = {
Just: 'Just',
Nothing: 'Nothing',
};
/**
A single instance of the `Nothing` object, to minimize memory usage. No matter
how many `Maybe`s are floating around, there will always be exactly and only
one `Nothing`.
@private
*/
let NOTHING;
// Defines the *implementation*, but not the *types*. See the exports below.
class MaybeImpl {
// SAFETY: this is definitely assigned in the constructor for every *actual*
// instance, but TS cannot see that: it is only set for `Nothing` instances
// when `NOTHING` does not already exist.
repr;
constructor(value) {
if (isVoid(value)) {
// SAFETY: there is only a single `Nothing` in the system, because the
// only difference between `Nothing<string>` and `Nothing<number>` is at
// the type-checking level.
if (!NOTHING) {
this.repr = [Variant.Nothing];
NOTHING = this;
}
return NOTHING;
}
else {
this.repr = [Variant.Just, value];
}
}
// Then the implementation signature is simply the same as the last two,
// overloads because we do not *and cannot* prevent the undesired function
// types from appearing here at runtime: doing so would require having a value
// on which to (maybe) apply the function!
static of(value) {
return new Maybe(value);
}
// The runtime signature *does* allow null and undefined values so that the
// body can correctly throw at runtime in the case where a caller passes data
// whose type lies about the contained value.
static just(value) {
if (isVoid(value)) {
throw new Error(`attempted to call "just" with ${value}`);
}
return new Maybe(value);
}
/**
Create an instance of `Maybe.Nothing`.
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:
```ts
const notString = Maybe.nothing<string>();
```
@template T The type of the item contained in the `Maybe`.
@returns An instance of `Maybe.Nothing<T>`.
*/
static nothing(_) {
return new MaybeImpl();
}
/** Distinguish between the `Just` and `Nothing` {@link Variant variants}. */
get variant() {
return this.repr[0];
}
/**
The wrapped value.
@warning throws if you access this from a {@linkcode Just}
*/
get value() {
if (this.repr[0] === Variant.Nothing) {
throw new Error('Cannot get the value of `Nothing`');
}
return this.repr[1];
}
/** Is the {@linkcode Maybe} a {@linkcode Just}? */
get isJust() {
return this.repr[0] === Variant.Just;
}
/** Is the {@linkcode Maybe} a {@linkcode Nothing}? */
get isNothing() {
return this.repr[0] === Variant.Nothing;
}
/** Method variant for {@linkcode map} */
map(mapFn) {
return this.repr[0] === 'Just' ? Maybe.just(mapFn(this.repr[1])) : nothing();
}
/** Method variant for {@link mapOr|`mapOr`} */
mapOr(orU, mapFn) {
return this.repr[0] === 'Just' ? mapFn(this.repr[1]) : orU;
}
/** Method variant for {@linkcode mapOrElse} */
mapOrElse(orElseFn, mapFn) {
return this.repr[0] === 'Just' ? mapFn(this.repr[1]) : orElseFn();
}
/** Method variant for {@linkcode match} */
match(matcher) {
return this.repr[0] === 'Just' ? matcher.Just(this.repr[1]) : matcher.Nothing();
}
/** Method variant for {@linkcode or} */
or(mOr) {
return this.repr[0] === 'Just' ? this : mOr;
}
orElse(orElseFn) {
return (this.repr[0] === 'Just' ? this : orElseFn());
}
/** Method variant for {@linkcode and} */
and(mAnd) {
return (this.repr[0] === 'Just' ? mAnd : this);
}
andThen(andThenFn) {
return (this.repr[0] === 'Just' ? andThenFn(this.repr[1]) : this);
}
/** Method variant for {@linkcode unwrapOr} */
unwrapOr(defaultValue) {
return this.repr[0] === 'Just' ? this.repr[1] : defaultValue;
}
/** Method variant for {@linkcode unwrapOrElse} */
unwrapOrElse(elseFn) {
return this.repr[0] === 'Just' ? this.repr[1] : elseFn();
}
/** Method variant for {@linkcode toString} */
toString() {
return this.repr[0] === 'Just' ? `Just(${safeToString(this.repr[1])})` : 'Nothing';
}
/** Method variant for {@linkcode toJSON} */
toJSON() {
const variant = this.repr[0];
// Handle nested Maybes
if (variant === 'Just') {
const value = isInstance(this.repr[1]) ? this.repr[1].toJSON() : this.repr[1];
return { variant, value };
}
else {
return { variant };
}
}
/** Method variant for {@linkcode equals} */
equals(comparison) {
return (this.repr[0] === comparison.repr[0] &&
this.repr[1] === comparison.repr[1]);
}
/** Method variant for {@linkcode ap} */
ap(val) {
return val.andThen((val) => this.map((fn) => fn(val)));
}
/**
Method variant for {@linkcode get}
If you have a `Maybe` of an object type, you can do `thatMaybe.get('a key')`
to look up the next layer down in the object.
```ts
type DeepOptionalType = {
something?: {
with?: {
deeperKeys?: string;
}
}
};
const fullySet: DeepType = {
something: {
with: {
deeperKeys: 'like this'
}
}
};
const deepJust = Maybe.of(fullySet)
.get('something')
.get('with')
.get('deeperKeys');
console.log(deepJust); // Just('like this');
const partiallyUnset: DeepType = { something: { } };
const deepEmpty = Maybe.of(partiallyUnset)
.get('something')
.get('with')
.get('deeperKeys');
console.log(deepEmpty); // Nothing
```
*/
get(key) {
return this.andThen(property(key));
}
}
/**
Create a {@linkcode Maybe} instance which is a {@linkcode Just}.
`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>`.
@template T The type of the item contained in the `Maybe`.
@param value The value to wrap in a `Maybe.Just`.
@returns An instance of `Maybe.Just<T>`.
@throws If you pass `null` or `undefined`.
*/
export const just = MaybeImpl.just;
/**
Is the {@linkcode Maybe} a {@linkcode Just}?
@template T The type of the item contained in the `Maybe`.
@param maybe The `Maybe` to check.
@returns A type guarded `Just`.
*/
export function isJust(maybe) {
return maybe.isJust;
}
/**
Is the {@linkcode Maybe} a {@linkcode Nothing}?
@template T The type of the item contained in the `Maybe`.
@param maybe The `Maybe` to check.
@returns A type guarded `Nothing`.
*/
export function isNothing(maybe) {
return maybe.isNothing;
}
/**
Create a {@linkcode Maybe} instance which is a {@linkcode Nothing}.
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:
```ts
const notString = Maybe.nothing<string>();
```
@template T The type of the item contained in the `Maybe`.
@returns An instance of `Maybe.Nothing<T>`.
*/
export const nothing = MaybeImpl.nothing;
/**
Create a {@linkcode 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:
```ts
import * as maybe from 'true-myth/maybe';
const foo = maybe.of<string>(null);
```
This is usually only important in two cases:
1. If you are intentionally constructing a `Nothing` from a known `null` or
undefined value *which is untyped*.
2. If you are specifying that the type is more general than the value passed
(since TypeScript can define types as literals).
@template T The type of the item contained in the `Maybe`.
@param value 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.
*/
export const of = MaybeImpl.of;
export function map(mapFn, maybe) {
const op = (m) => m.map(mapFn);
return curry1(op, maybe);
}
export function mapOr(orU, mapFn, maybe) {
function fullOp(fn, m) {
return m.mapOr(orU, fn);
}
function partialOp(fn, curriedMaybe) {
return curriedMaybe !== undefined
? fullOp(fn, curriedMaybe)
: (extraCurriedMaybe) => fullOp(fn, extraCurriedMaybe);
}
return mapFn === undefined
? partialOp
: maybe === undefined
? partialOp(mapFn)
: partialOp(mapFn, maybe);
}
export function mapOrElse(orElseFn, mapFn, maybe) {
function fullOp(fn, m) {
return m.mapOrElse(orElseFn, fn);
}
function partialOp(fn, curriedMaybe) {
return curriedMaybe !== undefined
? fullOp(fn, curriedMaybe)
: (extraCurriedMaybe) => fullOp(fn, extraCurriedMaybe);
}
if (mapFn === undefined) {
return partialOp;
}
else if (maybe === undefined) {
return partialOp(mapFn);
}
else {
return partialOp(mapFn, maybe);
}
}
export function and(andMaybe, maybe) {
const op = (m) => m.and(andMaybe);
return curry1(op, maybe);
}
export function andThen(thenFn, maybe) {
const op = (m) => m.andThen(thenFn);
return maybe !== undefined ? op(maybe) : op;
}
export function or(defaultMaybe, maybe) {
const op = (m) => m.or(defaultMaybe);
return maybe !== undefined ? op(maybe) : op;
}
export function orElse(elseFn, maybe) {
const op = (m) => m.orElse(elseFn);
return curry1(op, maybe);
}
export function unwrapOr(defaultValue, maybe) {
const op = (m) => m.unwrapOr(defaultValue);
return curry1(op, maybe);
}
export function unwrapOrElse(orElseFn, maybe) {
const op = (m) => m.unwrapOrElse(orElseFn);
return curry1(op, maybe);
}
/**
Create a `String` representation of a {@linkcode Maybe} instance.
A {@linkcode 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` |
@template T The type of the wrapped value; its own `.toString` will be used
to print the interior contents of the `Just` variant.
@param maybe The value to convert to a string.
@returns The string representation of the `Maybe`.
*/
export function toString(maybe) {
return maybe.toString();
}
/**
* Create an `Object` representation of a {@linkcode Maybe} instance.
*
* Useful for serialization. `JSON.stringify()` uses it.
*
* @param maybe The value to convert to JSON
* @returns The JSON representation of the `Maybe`
*/
export function toJSON(maybe) {
return maybe.toJSON();
}
/**
* Given a {@linkcode MaybeJSON} instance, convert it into a {@linkcode Maybe}.
*
* @param json The value to convert to JSON
* @returns The JSON representation of the `Maybe`
*/
export function fromJSON(json) {
return json.variant === Variant.Just ? just(json.value) : nothing();
}
export function match(matcher, maybe) {
const op = (curriedMaybe) => curriedMaybe.match(matcher);
return curry1(op, maybe);
}
export function equals(mb, ma) {
const op = (maybeA) => maybeA.equals(mb);
return curry1(op, ma);
}
export function ap(maybeFn, maybe) {
const op = (m) => maybeFn.ap(m);
return curry1(op, maybe);
}
export function isInstance(item) {
return item instanceof Maybe;
}
export function find(predicate, array) {
const op = (a) => Maybe.of(a.find(predicate));
return curry1(op, array);
}
export function first(array) {
return array.length !== 0 ? Maybe.just(Maybe.of(array[0])) : Maybe.nothing();
}
export function last(array) {
return array.length !== 0 ? Maybe.just(Maybe.of(array[array.length - 1])) : Maybe.nothing();
}
/**
Given an array or tuple of {@linkcode Maybe}s, return a `Maybe` of the array
or tuple values.
- Given an array of type `Array<Maybe<A> | Maybe<B>>`, the resulting type is
`Maybe<Array<A | B>>`.
- Given a tuple of type `[Maybe<A>, Maybe<B>]`, the resulting type is
`Maybe<[A, B]>`.
If any of the items in the array or tuple are {@linkcode Nothing}, the whole
result is `Nothing`. If all items in the array or tuple are {@linkcode Just},
the whole result is `Just`.
## Examples
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.
```ts
import Maybe, { transposeArray } from 'true-myth/maybe';
let valid = [Maybe.just(2), Maybe.just('three')];
let allJust = transposeArray(valid); // => Just([2, 'three']);
let invalid = [Maybe.just(2), Maybe.nothing<string>()];
let mixed = transposeArray(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`:
```ts
import Maybe, { transposeArray } from 'true-myth/maybe';
type Tuple = [Maybe<string>, Maybe<number>];
let invalid: Tuple = [Maybe.just('wat'), Maybe.nothing()];
let result = transposeArray(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]`:
```ts
import Maybe, { transposeArray } from 'true-myth/maybe';
type Tuple = [Maybe<string>, Maybe<number>];
let valid: Tuple = [Maybe.just('hey'), Maybe.just(12)];
let result = transposeArray(valid); // => Just(['hey', 12])
```
@param maybes The `Maybe`s to resolve to a single `Maybe`.
*/
export function transposeArray(maybes) {
// The slightly odd-seeming use of `[...ms, m]` here instead of `concat` is
// necessary to preserve the structure of the value passed in. The goal is for
// `[Maybe<string>, [Maybe<number>, Maybe<boolean>]]` not to be flattened into
// `Maybe<[string, number, boolean]>` (as `concat` would do) but instead to
// produce `Maybe<[string, [number, boolean]]>`.
return maybes.reduce((acc, m) => acc.andThen((ms) => m.map((m) => [...ms, m])), just([]));
}
export function property(key, obj) {
const op = (t) => Maybe.of(t[key]);
return curry1(op, obj);
}
export function get(key, maybeObj) {
return curry1(andThen(property(key)), maybeObj);
}
/**
Transform a function from a normal JS function which may return `null` or
`undefined` to a function which returns a {@linkcode Maybe} instead.
For example, dealing with the `Document#querySelector` DOM API involves a
*lot* of things which can be `null`:
```ts
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:
```ts
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 `safe`, 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`:
```ts
import { safe } from 'true-myth/maybe';
const querySelector = safe(document.querySelector.bind(document));
const safelyGetStyle = safe(getStyle);
const aWidth = querySelector('#foo')
.map(el => el.getBoundingClientRect().width)
.unwrapOr(0);
const aColor = querySelector('.bar')
.andThen(el => safelyGetStyle(el, 'color'))
.unwrapOr('black');
```
@param fn The function to transform; the resulting function will have the
exact same signature except for its return type.
*/
export function safe(fn) {
return (...params) => Maybe.of(fn(...params));
}
// Duplicate documentation because it will show up more nicely when rendered in
// TypeDoc than if it applies to only one or the other; using `@inheritdoc` will
// also work but works less well in terms of how editors render it (they do not
// process that “directive” in general).
/**
* `Maybe` represents a value which may ({@linkcode Just `Just<T>`}) or may not
* ({@linkcode Nothing}) be present.
*
* @class
*/
export const Maybe = MaybeImpl;
export default Maybe;
//# sourceMappingURL=maybe.js.map