1.0.2-dev.bbcda1167 • Published 4 years ago

@cazoo/maybe v1.0.2-dev.bbcda1167

Weekly downloads
-
License
MIT
Repository
-
Last release
4 years ago

@cazoo/maybe

About

What is this?

This provides some utility types that help represent uncertain outcomes. More specifically: it provides TypeScript implementations of result and option types.

The API for these types is very closely modelled around the Option and Result types in Rust. These Rust types are the inspiration for this library.

We also provide some helper types for dealing with options and results in asynchronous contexts. See AsyncOption and AsyncResult for more information.

Documentation

Documentation (including an API reference) is available in TSDoc format. We are working on a way of hosting this documentation.

What is an option?

An option represents a value that may or may not exist.

For example:

const nonEmpty = [1, 2, 3];
nonEmpty.pop(); // 3

const empty = [];
empty.pop(); // undefined

We could use an option to represent the return type of pop, because it might not produce a value.

We could model pop as follows:

interface Array<T> {
    pop(): Option<T>;
}

What is a result?

A result represents the outcome of an operation that might succeed (with some value), or fail (with some error).

Results are appropriate in any situation where we have a good reason to believe that an operation may fail. For example: we should always expect that file I/O and HTTP calls may fail.

We might use a result as follows:

interface Item {}

type DatabaseError =
    | "cannot-connect"
    | "table-not-found"
    | "item-not-found";

interface DatabaseClient {
    // result either:
    // - succeeds with Item
    // - fails with DatabaseError
    getItem(id: string): Result<Item, DatabaseError>
}

Why would I want to use these in my code?

There are many ways to model uncertainty in JavaScript:

To represent values that may not exist:

  • value or undefined
  • value or null
  • using an "empty" value, e.g. an empty array
  • using a discriminated union

To represent operations that may fail:

  • throwing an error
  • using a discriminated union
  • return a value that may not exist

Options and results are a clear and consistent way of representing these things. They provide a functional interface inspired by Rust's much-loved types.

JavaScript is a strange beast. Options and results make it difficult to mistake a value for an empty value and vice versa.

This cannot happen with an option:

const maybeNumber: number | undefined = 0;

if (!maybeNumber) {
    // oops! we meant to check for undefined, but 0
    // is a valid value in this case!
    throw new Error("value missing");
}

Options can make code more concise:

// without options
function parseAndDouble(n: string | null): number | null {
    if (n === null) {
        return null;
    }
    
    const parsed = parseInt(n);
    return Number.isNaN(parsed) ? null : parsed * 2;
}

// with options
function parseAndDouble(n: Option<string>): Option<number> {
    return n
        .map(parseInt)
        .filter(v => !Number.isNaN(v))
        .map(v => v * 2);
}

What are asynchronous options and results?

Promises make it a pain to work with the synchronous option and result types. For example:

// generate a promise of a result 
const result: Promise<Result<T, E>> = someResult();

// create another promise of a result using that data
const next = (await result).then(data => someOtherResult(data));

// and again...
(await next).then(data => someFurtherResult(data));

Asynchronous options and results use promises internally to abstract away this complexity. This allows you to write more concise code. The await calls above are no longer required.

someResult() // returns AsyncResult<T, E>
    .then(someOtherResult)
    .then(someFurtherResult)

Contributing

Contributions are very welcome! To contribute, first set up the repository. There are instructions for this below.

Open a branch and make whatever your changes. When you're ready, open a pull request for us to review!

When contributing, make sure that any additions are covered by unit tests. If you add or modify code, please ensure that you update the documentation to match your changes.

Setting up the code

First clone the repository:

$ git clone git@github.com:Cazoo-uk/maybe.git

Install dependencies:

$ cd path/to/maybe
$ npm install

Running tests

$ npm test