JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • 0
  • Score
    100M100P100Q33432F
  • License ISC

A small package to handle errors as values

Package Exports

  • resultable
  • resultable/dist/index.js
  • resultable/dist/index.mjs

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (resultable) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Resultable

A small package to handle errors as values

import { Match, Result } from "resultable";

class UserNotFound extends Result.BrandedError("UserNotFound") {}
class UserServiceUnavailable extends Result.BrandedError("UserServiceUnavailable") {}

declare const [user, userError]: Result.Result<{id: 1; name: string}, UserNotFound|UserServiceUnavailable>;

if (userError) {
    Match.matchBrand(userError)({
        "UserNotFound": () => console.log("User not found"),
        "UserServiceUnavailable": () => console.log("User service unavailable")
    })
} else {
    console.log("User", user);
}

Features

  • Result type in the form of a tuple [value, error]
  • BrandedErrors to differentiate between different errors
  • Pattern matching on errors

Installation

npm install resultable

Base types

type BaseError<T extends string> = Error & {
  readonly [TypeId]: TypeId;
  readonly __brand: T;
};
type OkResult<T> = Readonly<[value: T, error: undefined]>;
type ErrorResult<E extends BaseError<string>> = Readonly<[value: undefined, error: E]>;
type Result<T, E extends BaseError<string>> = OkResult<T> | ErrorResult<E>;

Result.BrandedError

The base error from which all resultable errors must extend. It adds a __brand readonly property to differentiate between different type of errors.

Each "brand" must be unique to make pattern matching work, we recommend using the path of the file plus the class name for the brand, for example: Result.BrandedError("@Users/Errors/UserNotFound")

Result.BrandedError: <T extends string>(brand: T) => new (...args: any) => BaseError<T>
import { Result } from "resultable";

class UserNotFound extends Result.BrandedError("UserNotFound") {}

Result.ok, Result.err, Result.okVoid

Results are just tuples with either value or error but we provide contructors to easily identify if your creating an ok result or an error result.

import { Result } from "resultable";

const okResult = Result.ok(1);
const errResult = Result.err(new Result.UnknownException("Unkown error"));
const okVoidResult = Result.okVoid();

Result.tryCatch

tryCatch is usefull to prevent functions from throwing.

It returns an UnknownException by default but you can customize the error.

function tryCatch<T>(fn: () => Promise<T>): Promise<Result<T, UnknownException>>;

function tryCatch<T, E extends BaseError<string>>(
  fn: () => Promise<T>,
  errorFn: (cause: unknown) => E,
): Promise<Result<T, E>>;
import { Result } from "resultable";

const fetchTest: Promise<Result.Result<Response, Result.UnknownException>> = Result.tryCatch(
    () => fetch("https://api.test.com")
);

class FetchError extends Result.BrandedError("FetchError") {
    constructor(public readonly cause: unknown) {
        super();
    }
}

const fetchTest2: Promise<Result.Result<Response, FetchError>> = Result.tryCatch(
    () => fetch("https://api.test.com"),
    (cause) => new FetchError(cause)
);

Result.resultableFn

It's an identity function to force you to always return results or branded errors

const resultableFn: <Params extends any[], TUnion extends OkResult<any> | ErrorResult<BaseError<string>> | BaseError<string>>(fn: (...args: Params) => Promise<TUnion>) => ((...args: Params) => Promise<MergeResults<NormalizeResult<TUnion>>>)
import { Result } from "resultable";

// Valid code
const createUser = Result.resultableFn(async function(name: string) {
    if (name.length < 3) {
        return Result.err(new Result.UnknownException("Name must be at least 3 characters"));
    }

    if (name === "not-allowed") {
        return new Result.UnknownException("Name not allowed");
    }
    
    return Result.ok({name})
});

const userResult = await createUser("John Doe");
// userResult type -> Result.Result<{ name: string; }, Result.UnknownException>

// Invalid code
// Type Error: '{ name: string; }' is not assignable to type 'readonly [value: any, error: undefined] | readonly [value: undefined, error: BaseError<string>]'.
const createUser2 = Result.resultableFn(async function(name: string) {
    if (name.length < 3) {
        return Result.err(new Result.UnknownException("Name must be at least 3 characters"));
    }
    
    return {name}
});