Skip to content

Well-typed functional Maybe monad in typescript

License

Notifications You must be signed in to change notification settings

andnp/MaybeTyped

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MaybeTyped

Build Status codecov semantic-release Known Vulnerabilities

MaybeTyped is a well-typed Maybe (optional) monad written in typescript.

npm install maybetyped

Usage Examples

import Maybe, { some, none, maybe } from 'maybetyped';

function getUsername(): Maybe<string> {
    return maybe(usernameElement.text());
}

const normalizedUsername =
    getUsername()
        .map(name => name.split(' ')[0])
        .map(name => name.toLowerCase())
        .orElse('username');

// without Maybe
function getUsername(): string | undefined {
    return usernameElement.text();
}

let normalizedUsername = 'username';
const username = getUsername();
if (username !== undefined) {
    const firstName = username.split(' ')[0];
    normalizedUsername = firstName.toLowerCase();
}

Api

map

Map gives access to the contained value. Imagine an array, Array<string>, as a container for strings, the map function applies a function to each element if the container is not empty and gives back a new container. For instance:

const orig: Array<string> = ['1', '2', '3'];
const now: Array<number> = orig.map(x => parseInt(x));
    some('thing').map(v => console.log(v)) // prints "thing"

    none().map(v => console.log(v)) // does not print

tap

Like map, it calls a function with the current value, if there is one, but ignores any return value and the result is always the original Maybe. Intended for running side-effects, without changing the value.

some(1)
    // If this was `.map`, then the result of this call would be None
    .tap(x => console.log(`Original value is ${x}`))
    .map(x => x + 1)
    .tap(x => console.log(`New value is ${x}`))

flatMap

FlatMap also accesses the contained value, but it expects that its "mapper" function returns a container of the same type. Imagine the conceptually equivalent array container:

const orig: Array<number> = [1, 3, 5];
const now: Array<number> = orig.flatMap(x => ( [x, x + 1] ));
console.log(now); // => [1, 2, 3, 4, 5, 6]
const maybeAdd1 = (x: Maybe<number>) => x.map(y => y + 1);

const x = some(2).flatMap(maybeAdd1); // Maybe<3>

const y = none().flatMap(maybeAdd1); // Maybe<undefined>

or

Similar to the or logical operator. Tries to get the value (true) of the first maybe; if it is empty (false), tries to get the value (true) of the second maybe. If both are empty (false), returns an empty (false) maybe.

const first = none();
const second = some(22);

const third = first.or(second); // Maybe<22>

orElse

Similar to or, except the second value is not allowed to be empty. orElse must return an instance of the contained value, even if the maybe is empty. This is useful for supplying default values:

const maybeName = maybe(getNameFromInput());
const name = maybeName.orElse('enter name please');
const first = none<string>();
const second = 'hi';

const third = first.orElse(second); // 'hi';

expect

expect forcefully gets the value out of the Maybe container, or throws an error if there is no value. This is useful whenever you know the value must be defined at this point, and you want to get out of the Maybe chain. For instance:

function tryOption1(): Maybe<string> { ... }
function tryOption2(): Maybe<string> { ... }
function tryOption3(): Maybe<string> { ... } // The string must be created by one of these 3, we just don't know which

const str: string =
    tryOption1()
        .or(tryOption2)
        .or(tryOption3)
        .expect('We expected to get the from one of these three methods');
function getData(): Maybe<DataType> { ... }
const maybeData = getData();

const shouldHaveData = maybeData.expect("oops, guess I didn't");
// throws an error with the given message if value is null
// otherwise returns value

caseOf

caseOf is a pattern matcher for the Maybe. This is useful when you want to execute different logic dependent on whether the container is empty. For instance:

maybeData.caseOf({
    none: () => attemptToGetFromApi().map(doThingWithData),
    some: data => doThingWithData(data),
});
getData().caseOf({
    some: value => value + 1,
    none: () => 1,
});
// executes the "some" function if not null
// executes the "none" function if null

asNullable

asNullable provides an "out" for escaping the Maybe container. This is particularly useful at the boundaries of your API. Often the internals of a library use Maybe to clean up code, but would like their external contracts to not be forced to use Maybes, but instead "vanilla" JS. For instance:

export function doThing(): string | undefined {
    const maybeValue: Maybe<string> = getFromSomewhereInLib();
    return maybeValue.asNullable();
}
const value = 'hi';
const nullable = maybe(value).asNullable();

assert(nullable === value);

join

join takes a "joiner" function and another Maybe instance and combines them. If either of the Maybes are empty, then the joiner function is not called.

const first = maybe(getFirstName());
const last = maybe(getLastName());

const name_007 = first.join(
    (a, b) => `${b}. ${a} ${b}.`,
    last,
);

MaybeT

export function apiUserSearch(user: string): MaybeT<Promise<UserData>> {
    // if user does not exist, api returns undefined
    return maybeT(fetch(`some/uri?user=${user}`).json());
}

const userBirthday = await apiUserSearch('yagami')
    .map(user => user.birthday)
    .map(date => new Date(date))
    .orElse(() => Date.now()); // <- this is probably a bad design choice :P

const userBirthdayPromises = maybeT(['misa misa', 'light', null, 'ryuk'])
    .map(apiUserSearch)
    .map(maybeUser =>
        maybeUser
            .map(user => user.birthday)
            .map(date => new Date(date))
            .orElse(() => Date.now()))
    .asNullable();

const userBirthdays = await Promise.all(userBirthdayPromises);

Api

maybeT

maybeT is the constructor for a maybe transform. Anything with a map function can be transformed into a maybeT. Due to the commonality of the use case, support for thenables is also added, though be warned that then matches flatMap semantics, not map semantics.

const maybeThings = maybeT([1, 2, null, 4, undefined, 6]); // MaybeT<Array<number>>
const maybeLater = maybeT(Promise.resolve('hey')); // MaybeT<Promise<string>>

map

const things = maybeT(['1', '2', null, '4']) // MaybeT<Array<string>>
    .map(x => parseInt(x)); // MaybeT<Array<number>>

caseOf

const things = maybeT([1, 2, null, 4])
    .caseOf({
        none: () => 4,
        some: x => x + 1,
    }); // MaybeT<Array<number>> => MaybeT<[2, 3, 4, 5]>

orElse

const things = maybeT([1, 2, null, 4])
    .orElse(3); // MaybeT<Array<number>> => MaybeT<[1, 2, 3, 4]>

asNullable

const things = maybeT([1, 2, null, 4])
    .asNullable(); // Array<number> => [1, 2, null, 4]

asType

Because typescript does not have support for higher-kinded-types (HKT), we lose track of which monad-like HKT we are dealing with (Array or Promise or other). This means that after most operations the type will become MaybeT<MonadLike<*>>. To cope with this, we provide an asType method that will allow us to properly "remember" what type of monad we were originally dealing with. A little type safety will be lost here, as you could lie and say this is an Array instead of a Promise, but the constructor that is passed in to this method will confirm the type at runtime. This method also asks for the contained type, but because we haven't forgotten that, we will be able to check that.

Programmatic examples below should help make this more clear.

const a = maybeT(Promise.resolve('hi'))
    .asType<Promise<string>>(Promise); // Promise<string> => this is correct

const b = maybeT(Promise.resolve('hey'))
    .asType<Array<string>>(Array); // Array<string> => this will throw a runtime err, but not a compile err

const c = maybeT(Promise.resolve('hello'))
    .asType<Promise<number>>(Promise); // any => this will throw a compile err, but not runtime

const d = maybeT(Promise.resolve('merp'))
    .asType<Promise<string>>(Array); // any => this will throw a compile err and runtime