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

ts-failable

v0.6.1

Published

Library for type safe error handling in Typescript

Downloads

1,502

Readme

ts-failable

Build Status npm version Issues

This is a library for type safe error/null handling in Typescript. If you've ever wished for Rust's Result type along with the syntactic sugar (? operator) in Typescript, you might find this useful.

API Docs

https://dhruvrajvanshi.github.io/ts-failable/docs/index.html

Installation

npm install ts-failable

For type safe optionals, you need Typescript version >= 2.8.1 because it uses conditional types. Also, your runtime needs to support Proxy support, which is not supported in IE.

For IE support, there would be a less transparent version of Optional in the future.

Optional

Optional type is exposed which lets you access deeply nested nullable properties as if each intermediate object was defined.

import { Optional } from "ts-failable/optional";
type T = {
  x?: {
    y?: {
      z: string;
    }
  }
}
const t: T = {};
const optionalT = Optional.of(t)
console.log(optionalT.x.y.z.valueOf()) // null
const optionalT1 = Optional.of<T>({
  x: {
    y: {
      z: "asdf"
    }
  }
})
console.log(optionalT.x.y.z.valueOf()) // asdf

So, any null/undefined value along the path short circuits the evaluation. If you're familiar with Kotlin, this is similar to

optionalT.x?.y?.z

Caveats

  • Currently, this only works on immutable objects. Mutating the object might have unintended consequences. This restriction will be removed in a later version. Submit a PR if you want this now :)
  • Because it iternally uses proxy, each intermediate lookup involves an extra heap lookup and a string comparison (to check against "valueOf") so, if you're using this in a hot loop with a lot of nested keys, it will cost you.
  • Properties that have type Something | undefined are converted to null by value of so you only have to check for null.
  • Conditional types are required so use Typescript version >= 2.8.1

Result

Why

In Typescript, there's no way to check the type of exceptions that a function can throw at compile time. When you catch an exception, you get a value of type any. You can encode a result type Either/Result type like Haskell or Rust that can either be a success value or failure value. Unlike Haskell and Rust, Typescript doesn't have special syntax to make chaining of Result values easier.

This library helps in reducing the boilerplate related to error handling in a type safe manner. Think of it like async/await syntax for error handling. If you're familiar with Haskell or Scala, you know this as do notation or for/yield syntax both of which allow you to chain failable computations without nesting using flatMap or >>=.

In short, instead of this

let result = computation1()
  .flatMap(r1 =>
    computation2(r1)
      .flatMap(r2 => computation3(r1, r2))
  )

We would like to write this

let r1 = run(computation1());
let r2 = run(computation2(r1));
let r3 = run(computation3(r1, r2))

Any failure in an intermediate step should short circuit the whole thing.

Usage

We'll walk through a simple function named getNumber that takes an optional string and returns a number if the string is present, contains an integer and the integer is greater than 10. If any of these conditions fail, it should return a description of the error.

Start with importing a few things

import { Failable, failable } from "ts-failable";

Lets start with defining types for each of our failure cases.

type NOT_FOUND = {
  type: "NOT_FOUND"
}
type NOT_A_NUMBER = {
  type: "NOT_A_NUMBER";
  str: string;
}
type TOO_SMALL = {
  type: "TOO_SMALL";
  value: number;
}

Our function would can throw one of these error types so let's name the error type of our function.

type GetNumberError = NOT_FOUND | NOT_A_NUMBER | TOO_SMALL;

Our function needs to take a string | undefined argument and needs to return something that will have a number in case all the conditions are met, or a GetNumberError. This can be encoded in ts-failure by IFailable<number, GetNumberError>.

Our function is really small but it has three distinct steps. To demonstrate chaining, I'll make each step as a seperate function.

const getString = (str: string | undefined) =>
  failable<string, NOT_FOUND>(({ success, failure }) => {
    if (str !== undefined) {
      return success(str);
    } else {
      return failure({ type: NOT_FOUND });
    }
  });

const parseInteger = (str: string) =>
  failable<number, NOT_A_NUMBER>(({ success, failure }) => {
    const num = parseInt(str);
    if (num === NaN) {
      return failure({
        type: NOT_A_NUMBER,
        str: str
      });
    } else {
      return success(num);
    }
  });
const getNumber = (optionalString: string | undefined) =>
  failable<number, GetNumberError>(({ success, failure, run }) => {
    const str = run(getString(optionalString));
    const num = run(parseInteger(str));
    if (num < 10) {
      return failure({
        type: TOO_SMALL,
        value: num
      });
    } else {
      return success(num);
    }
  })

You can see that getNumber doesn't need to check for error at each step. It run will propagate the intermediate errors upwards just like exceptions.

Run will only accept computations whose failure type is a subtype of the current context's failure type so you have to declare downstream failures in the type.

To handle an IFailable<T, E>, you can use .match method to pattern match on the result.

const logError = (err: GetNumberError) => {
  if (err.type === "NOT_FOUND") {
    console.log("Value not found");
  } else if (err.type === "NOT_A_NUMBER") {
    console.log(`Expected ${err.str} to be a number.`);
  } else {
    console.log(`${err.value} is greater than 10`);
  }
};

const num = getNumber("10").match({
  success:value => value,
  failure:err => {
    logError(err);
    return 0;
  }
});

.match needs 2 functions that handle both success and failure cases. The success function receives the successful value of the computation and failure receives the error object that was thrown. The result type of both the functions must be the same.

Async

The the library exposes another function failableAsync that is useful for running asynchronous functions. If the intermediate steps in the previous example were asynchronous and returned promises, we could've written it like this.

import { failableAsync } from "ts-failable";

const getNumber = (optionalString: string | undefined) =>
  failableAsync<number, GetNumberError>(async ({ success, failure, run }) => {
    const str = run(await getString(optionalString));
    const num = run(await parseInteger(str));
    if (num < 10) {
      return failure({
        type: TOO_SMALL,
        value: num
      });
    } else {
      return success(num);
    }
  })

Only three things have been changed here.

  1. failable -> failableAsync
  2. Added async keyword to the argument function.
  3. Added await in front of arguments to run that return FailablePromise.

FailablePromise<T, E> is a type alias defined as.

type FailablePromise<T, E> = Promise<IFailable<T, E>>;

While dealing with APIs that deal with Promises and exceptions, you can wrap the functions that return Promises into FailablePromise returning functions like this.

type DB_ERROR = {
  type: "DB_ERROR";
  error: any; // or whatever the type of error your DB driver returns
}

const query = (q: string) =>
  failableAsync<Row[], DB_ERROR>(async ({ success, failure }) => {
    try {
      const rows = await db.query(q); // or whatever your DB driver exposes
      return success(rows);
    } catch (e) {
      return failure({
        type: "DB_ERROR",
        error: e
      });
    }
  })

Now, you can use the query function in any failableAsync context while ensuring that DB_ERROR is either handled by the caller or is propagated upwards.