Skip to content

Maybe

How Maybe works

🚧 Under Construction 🚧

There will be different, and better, content here Soon™. We didn’t want to block getting the new docs site live on having finished updating all the existing content!

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

ts
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!

typescript
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:

typescript
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:

typescript
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.