npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

typescript-result

v1.2.0

Published

Imagine we need a function that performs some kind of I/O task that might fail:

Downloads

781

Readme

NPM TYPESCRIPT BUNDLEPHOBIA Weekly downloads

Heavily inspired by the Rust and Kotlin counterparts, this utility helps you with code that might fail in a declarative way.

Why?

Imagine we need a function that performs some kind of I/O task that might fail:

function readStuffFromFile(path: string): string {
  let stuff: string;

  if (!fileDoesExist(path)) {
    throw new Error(`The file ${path} does not exist`);
  }

  // ... implementation here ...

  return stuff;
}

function app() {
  try {
    const stuff = readStuffFromFile("/my/path/to/file.txt");
  } catch (err) {
    console.error("Unable to read stuff!");
  }
}

The problem with this 'usual' try-catch approach is that:

  • it makes our code harder to reason about. We need to look at implementation details to discover what might go wrong.
  • it makes the control flow of our code harder to reason about, especially with multiple (nested) try-catch statements

Instead, we could express the outcome of code to be executed in the form of a Result-type. People using your code will be explicitly confronted with the fact that code potentially might fail, and will know upfront what kind of errors they can expect.

Installation

npm install --save typescript-result

or

yarn add typescript-result

Usage

typescript-result exposes a single type:

import { Result } from "typescript-result";

Basically Result is a container with a generic type: one for failure, and one for success:

Result<ErrorType, OkType>

Example

Let's refactor the readStuffFromFile() a bit:

import { Result } from "typescript-result";

class FileDoesNotExistError extends Error {}

function readStuffFromFile(
  path: string
): Result<FileDoesNotExistError | Error, string> {
  try {
    let stuff: string;

    if (!fileDoesExist(path)) {
      return Result.error(
        new FileDoesNotExistError(`The file ${path} does not exist`)
      );
    }

    // ... implementation here ...

    return Result.ok(stuff);
  } catch (e) {
    return Result.error(e);
  }
}

function app() {
  const result = readStuffFromFile("/my/path/to/file.txt");

  if (result.isSuccess()) {
    // we're on the 'happy' path!
  } else {
    switch (result.error.constructor) {
      case FileDoesNotExistError:
        // handle the error
        // i.e. inform the user
        break;
      default:
      // an unexpected error...
      // something might be seriously wrong
      // i.e. log this error somewhere
    }
  }
}

Static creation methods

Result.ok and Result.error

function doStuff(value: number): Result<Error, number> {
  if (value === 2) {
    return Result.error(new Error("Number 2 is not allowed!"));
  }

  return Result.ok(value * 2);
}

Result.safe

Functions as a try-catch, returning the return-value of the callback on success, or the predefined error(-class) or caught error on failure:

// with caught error...
const result = Result.safe(() => {
  let value = 2;

  // code that might throw...

  return value;
}); // Result<Error, number>

// with predefined error...
class CustomError extends Error {}

const result = Result.safe(new CustomError("Custom error!"), () => {
  let value = 2;

  // code that might throw...

  return value;
}); // Result<CustomError, number>

// with predefined error-class...
class CustomError extends Error {}

const result = Result.safe(CustomError, () => {
  let value = 2;

  // code that might throw...

  return value;
}); // Result<CustomError, number>

Result.combine

Accepts multiple Results or functions that return Results and returns a singe Result. Successful values will be placed inside a tuple.

class CustomError extends Error {}

function doA(): Result<Error, string> {}
function doB(value: number): Result<Error, number> {}
function doC(value: string): Result<CustomError, Date> {}

const result = Result.combine(
  doA(),
  () => doB(2),
  () => doC("hello")
); // Result<Error | CustomError, [string, number, Date]>

if (result.isSuccess()) {
  result.value; // [string, number, Date]
}

Result.wrap

Transforms an existing function into a function that returns a Result:

function add2(value: number) {
  // code that might throw....

  return value + 2;
}

const wrappedAdd2 = Result.wrap(add2);

const result1 = add2(4); // number;
const result2 = wrappedAdd2(4); // Result<Error, number>;

Instance methods of Result

Result.isSuccess()

Indicates whether the Result is of type Ok. By doing this check you gain access to the encapsulated value:

const result = doStuff();
if (result.isSuccess()) {
  result.value; // we now have access to 'value'
} else {
  result.error; // we now have access to 'error'
}

Result.isFailure()

Indicates whether the Result is of type Error. By doing this check you gain access to the encapsulated error:

const result = doStuff();
if (result.isFailure()) {
  result.error; // we now have access to 'error'
} else {
  result.value; // we now have access to 'value'
}

Result.errorOrNull()

Returns the error on failure or null on success:

// on failure...
const result = thisWillFail();
const error = result.errorOrNull(); // error is defined
// on success...
const result = thisWillSucceed();
const error = result.errorOrNull(); // error is null

Result.getOrNull()

Returns the value on success or null on failure:

// on success...
const result = thisWillSucceed();
const value = result.getOrNull(); // value is defined
// on failure...
const result = thisWillFail();
const value = result.getOrNull(); // value is null

Result.fold(onSuccess: (value) => T, onFailure: (error) => T);

Returns the result of the onSuccess-callback for the encapsulated value if this instance represents success or the result of onFailure-callback for the encapsulated error if it is failure:

const result = doStuff();
const value = result.fold(
  // on success...
  value => value * 2,
  // on failure...
  error => 4
);

Result.getOrDefault(value: T)

Returns the value on success or the return-value of the onFailure-callback on failure:

const result = doStuff();
const value = result.getOrDefault(2);

Result.getOrElse(fn: (error) => T)

Returns the value on success or the return-value of the onFailure-callback on failure:

const result = doStuff();
const value = result.getOrElse(error => 4);

Result.getOrThrow()

Returns the value on success or throws the error on failure:

const result = doStuff();
const value = result.getOrThrow();

Result.map()

Maps a result to another result. If the result is success, it will call the callback-function with the encapsulated value, which returnr another Result. If the result is failure, it will ignore the callback-function, and will return the initial Result (error)

class ErrorA extends Error {}
class ErrorB extends Error {}

function doA(): Result<ErrorA, number> {}
function doB(value: number): Result<ErrorB, string> {}

// nested results will flat-map to a single Result...
const result1 = doA().map(value => doB(value)); // Result<ErrorA | ErrorB, string>

// ...or transform the successful value right away
// note: underneath, the callback is wrapped inside Result.safe() in case the callback
// might throw
const result2 = doA().map(value => value * 2); // Result<ErrorA | Error, number>

Result.forward()

Creates and forwards a brand new Result out of the current error or value. This is useful if you want to return early after failure.

class ErrorA extends Error {}
class ErrorB extends Error {}

function doA(): Result<ErrorA, number> {}
function doB(): Result<ErrorB, number> {}

function performAction(): Result<ErrorA | ErrorB, number> {
  const resultA = doA();
  if (resultA.isFailure()) {
    return resultA.forward();
  }

  const resultB = doA();
  if (resultB.isFailure()) {
    return resultB.forward();
  }

  // from here both 'a' and 'b' are valid values
  const [a, b] = [resultA.value, resultB.value];

  return a + b;
}

Rollbacks

There are cases where a series of operations are performed that need to be treated as a 'unit of work'. In other words: if the last operation fails, de preceding operations should also fail, despite the fact that those preceding operations succeeded on their own. In such cases you probably want some kind of recovering a.k.a. a rollback.

Fortunately, typescript-result allows you to rollback your changes with the minimum amount of effort.

Example

In this example we're dealing with user-data that needs to be saved within one transaction:

async function updateUserThingA(
  userId: string,
  thingA: string
): Result<Error, null> {
  try {
    // get hold of the value we're about to update
    const { thingA: oldThingA } = await db.getUser(userId);

    // run the update
    await db.updateUser(userId, { thingA });

    // We return a successful Result, AND passing a rollback function as 2nd parameter
    return Result.ok(null, async () => {
      // restore 'thingA' to the old value
      await db.updateUser(userId, { thingA: oldThingA });
    });
  } catch (e) {
    return Result.error(e);
  }
}

async function updateUserThingB(
  userId: string,
  thingB: string
): Result<Error, null> {
  /* similar implementation as 'updateUserThingA' */
}

function updateUser(userId: string, thingA: string, thingB: string) {
  const result = await Result.combine(
    () => updateUserThingA(userId, thingA),
    () => updateUserThingB(userId, thingB)
  );

  if (result.isFailure()) {
    // We received a failing result, let's rollback!
    // Since rollbacks themselves can also fail, we also receive a Result indicating whether the rollback succeeded or not
    const rollbackResult = await result.rollback();

    if (rollbackResult.isFailure()) {
      // something is seriously wrong!
      return `Unexpected error!`;
    }

    return `Could not update the user :(`;
  }

  return "Successfully updated the user!";
}