Module public/maybe

Maybe

A Maybe<T> represents a value of type T which may, or may not, be present.

If the value is present, it is Just(value). If it's absent, it's Nothing. 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 nasty null and undefined 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 on Nothing instances with functions which require a T to behave properly, e.g. map, which cannot check that the map function satisfies the type constraints for Maybe<T> unless Nothing has a parameter T to constrain it on invocation.

Put simply: without the type parameter, if you had a Nothing variant of a Maybe<string>, and you tried to use it with a function which expected a Maybe<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 and Nothing types directly, so you may also write them in a more traditional "fluent" object style.

Examples: functional style

import Maybe from 'true-myth/maybe';

// Construct a `Just` where you have a value to use, and the function accepts
// a `Maybe`.
const aKnownNumber = Maybe.just(12);

// Construct a `Nothing` where you don't have a value to use, but the
// function requires a value (and accepts a `Maybe`).
const aKnownNothing = Maybe.nothing<string>();

// Construct a `Maybe` where you don't know whether the value will exist or
// not, using `of`.
type WhoKnows = { mightBeAThing?: boolean[] };

const whoKnows: WhoKnows = {};
const wrappedWhoKnows = Maybe.of(whoKnows.mightBeAThing);
console.log(toString(wrappedWhoKnows)); // Nothing

const whoElseKnows: WhoKnows = { mightBeAThing: [true, false] };
const wrappedWhoElseKnows = Maybe.of(whoElseKnows.mightBeAThing);
console.log(toString(wrappedWhoElseKnows)); // "Just(true,false)"

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 use Maybe.of – you can't know at that point whether it's safe to construct a Just without checking, but of will always work correctly!

import { isVoid } from 'true-myth/utils';
import Maybe, { Just, Nothing } from 'true-myth/maybe';

// Construct a `Just` where you have a value to use, and the function accepts
// a `Maybe`.
const aKnownNumber = new Just(12);

// Once the item is constructed, you can apply methods directly on it.
const fromMappedJust = aKnownNumber.map((x) => x * 2).unwrapOr(0);
console.log(fromMappedJust); // 24

// Construct a `Nothing` where you don't have a value to use, but the
// function requires a value (and accepts a `Maybe<string>`).
const aKnownNothing = new Nothing();

// The same operations will behave safely on a `Nothing` as on a `Just`:
const fromMappedNothing = aKnownNothing.map((x) => x * 2).unwrapOr(0);
console.log(fromMappedNothing); // 0

// Construct a `Maybe` where you don't know whether the value will exist or
// not, using `isVoid` to decide which to construct.
type WhoKnows = { mightBeAThing?: boolean[] };

const whoKnows: WhoKnows = {};
const wrappedWhoKnows = !isVoid(whoKnows.mightBeAThing)
? new Just(whoKnows.mightBeAThing)
: new Nothing();

console.log(wrappedWhoKnows.toString()); // Nothing

const whoElseKnows: WhoKnows = { mightBeAThing: [true, false] };
const wrappedWhoElseKnows = !isVoid(whoElseKnows.mightBeAThing)
? new Just(whoElseKnows.mightBeAThing)
: new Nothing();

console.log(wrappedWhoElseKnows.toString()); // "Just(true,false)"

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 new Maybe. If an API lies to you for some reason and hands you an undefined or a null (even though you expect it to be an actual T in a specific scenario), the .of() function will still construct it correctly for you.

By contrast, if you do Maybe.just(someVariable) and someVariable is null or undefined, the program will throw at that point. This is a simple consequence of the need to make the new Just() constructor work; we cannot construct Just safely in a way that excludes a type of Maybe<null> or Maybe<undefined> otherwise – and that would defeat the whole purpose of using a Maybe!

Writing type constraints

Especially when constructing a Nothing, you may need to specify what kind of Nothing it is. The TypeScript type system can figure it out based on the value passed in for a Just, but there's no value to use with a Nothing, so you may have to specify it. In that case, you can write the type explicitly:

import Maybe, { nothing } from 'true-myth/maybe';

function takesAMaybeString(thingItTakes: Maybe<string>) {
console.log(thingItTakes.unwrapOr(''));
}

// Via type definition
const nothingHere: Maybe<string> = nothing();
takesAMaybeString(nothingHere);

// Via type coercion
const nothingHereEither = nothing<string>();
takesAMaybeString(nothingHereEither);

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:

import Maybe, { nothing } from 'true-myth/maybe';

// ERROR: Type 'Maybe<{}>' is not assignable to type 'Maybe<number>'.
const getAMaybeNotAssignable = (): Maybe<number> => {
const theMaybe = nothing();
return theMaybe;
};

// Succeeds
const getAMaybeExpression = (shouldBeJust: boolean): Maybe<number> => nothing();

// Succeeds
const getAMaybeReturn = (shouldBeJust: boolean): Maybe<number> => {
return nothing();
};

Using Maybe Effectively

The best times to create and safely unwrap Maybes 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 a Maybe 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 the Maybe 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 a null 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 Maybes to replace boolean flags or similar approaches for defining branching behavior or output from a function. You should avoid that, in general. Reserve Maybe for modeling the possible absence of data, and nothing else. Prefer to use Result or Either 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.

References

Renames and re-exports Maybe

Generated using TypeDoc