0.1.1 • Published 1 year ago

@wunderwerk/ts-results v0.1.1

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

ts-results

A typescript implementation of Rust's Result and Option objects.

Brings compile-time error checking and optional values to typescript.

Contents

Installation

$ npm install ts-results

or

$ yarn add ts-results

Example

Result Example

Convert this:

import { existsSync, readFileSync } from "fs";

function readFile(path: string): string {
  if (existsSync(path)) {
    return readFileSync(path);
  } else {
    // Callers of readFile have no way of knowing the function can fail
    throw new Error("invalid path");
  }
}

// This line may fail unexpectedly without warnings from typescript
const text = readFile("test.txt");

To this:

import { existsSync, readFileSync } from "fs";
import { Ok, Err, Result } from "ts-results";

function readFile(path: string): Result<string, "invalid path"> {
  if (existsSync(path)) {
    return new Ok(readFileSync(path)); // new is optional here
  } else {
    return new Err("invalid path"); // new is optional here
  }
}

// Typescript now forces you to check whether you have a valid result at compile time.
const result = readFile("test.txt");
if (result.ok) {
  // text contains the file's content
  const text = result.val;
} else {
  // err equals 'invalid path'
  const err = result.val;
}

Option Example

Convert this:

declare function getLoggedInUsername(): string | undefined;

declare function getImageURLForUsername(username: string): string | undefined;

function getLoggedInImageURL(): string | undefined {
  const username = getLoggedInUsername();
  if (!username) {
    return undefined;
  }

  return getImageURLForUsername(username);
}

const stringUrl = getLoggedInImageURL();
const optionalUrl = stringUrl ? new URL(stringUrl) : undefined;
console.log(optionalUrl);

To this:

import { Option, Some, None } from "ts-results";

declare function getLoggedInUsername(): Option<string>;

declare function getImageForUsername(username: string): Option<string>;

function getLoggedInImage(): Option<string> {
  return getLoggedInUsername().andThen(getImageForUsername);
}

const optionalUrl = getLoggedInImage().map((url) => new URL(stringUrl));
console.log(optionalUrl); // Some(URL('...'))

// To extract the value, do this:
if (optionalUrl.some) {
  const url: URL = optionalUrl.val;
}

Usage

import { Result, Err, Ok } from "ts-results";

Creation

let okResult: Result<number, Error> = Ok(10);
let errorResult: Result<number, Error> = Err(new Error("bad number!"));

Type Safety

Note: Typescript currently has a bug, making this type narrowing only work when strictNullChecks is turned on.

let result: Result<number, Error> = Ok(1);
if (result.ok) {
  // Typescript knows that result.val is a number because result.ok was true
  let number = result.val + 1;
} else {
  // Typescript knows that result.val is an `Error` because result.ok was false
  console.error(result.val.message);
}

if (result.err) {
  // Typescript knows that result.val is an `Error` because result.err was true
  console.error(result.val.message);
} else {
  // Typescript knows that result.val is a number because result.err was false
  let number = result.val + 1;
}

Stack Trace

A stack trace is generated when an Err is created.

let error = Err("Uh Oh");
let stack = error.stack;

Unwrap

let goodResult = new Ok(1);
let badResult = new Err(new Error("something went wrong"));

goodResult.unwrap(); // 1
badResult.unwrap(); // throws Error("something went wrong")

Expect

let goodResult = Ok(1);
let badResult = Err(new Error("something went wrong"));

goodResult.expect("goodResult should be a number"); // 1
badResult.expect("badResult should be a number"); // throws Error("badResult should be a number - Error: something went wrong")

ExpectErr

let goodResult = Ok(1);
let badResult = Err(new Error("something went wrong"));

goodResult.expect("goodResult should not be a number"); // throws Error("goodResult should not be a number")
badResult.expect("badResult should not be a number"); // new Error('something went wrong')

Map and MapErr

let goodResult = Ok(1);
let badResult = Err(new Error("something went wrong"));

goodResult.map((num) => num + 1).unwrap(); // 2
badResult.map((num) => num + 1).unwrap(); // throws Error("something went wrong")

goodResult
  .map((num) => num + 1)
  .mapErr((err) => new Error("mapped"))
  .unwrap(); // 2
badResult
  .map((num) => num + 1)
  .mapErr((err) => new Error("mapped"))
  .unwrap(); // throws Error("mapped")

andThen

let goodResult = Ok(1);
let badResult = Err(new Error("something went wrong"));

goodResult.andThen((num) => new Ok(num + 1)).unwrap(); // 2
badResult.andThen((num) => new Err(new Error("2nd error"))).unwrap(); // throws Error('something went wrong')
goodResult.andThen((num) => new Err(new Error("2nd error"))).unwrap(); // throws Error('2nd error')

goodResult
  .andThen((num) => new Ok(num + 1))
  .mapErr((err) => new Error("mapped"))
  .unwrap(); // 2
badResult
  .andThen((num) => new Err(new Error("2nd error")))
  .mapErr((err) => new Error("mapped"))
  .unwrap(); // throws Error('mapped')
goodResult
  .andThen((num) => new Err(new Error("2nd error")))
  .mapErr((err) => new Error("mapped"))
  .unwrap(); // thros Error('mapped')

Else

Deprecated in favor of unwrapOr

UnwrapOr

let goodResult = Ok(1);
let badResult = Err(new Error("something went wrong"));

goodResult.unwrapOr(5); // 1
badResult.unwrapOr(5); // 5

Empty

function checkIsValid(isValid: boolean): Result<void, Error> {
  if (isValid) {
    return Ok.EMPTY;
  } else {
    return new Err(new Error("Not valid"));
  }
}

Combining Results

ts-results has two helper functions for operating over n Result objects.

Result.all

Either returns all of the Ok values, or the first Err value

let pizzaResult: Result<Pizza, GetPizzaError> = getPizzaSomehow();
let toppingsResult: Result<Toppings, GetToppingsError> = getToppingsSomehow();

let result = Result.all(pizzaResult, toppingsResult); // Result<[Pizza, Toppings], GetPizzaError | GetToppingsError>

let [pizza, toppings] = result.unwrap(); // pizza is a Pizza, toppings is a Toppings.  Could throw GetPizzaError or GetToppingsError.
Result.any

Either returns the first Ok value, or all Err values

let url1: Result<string, Error1> = attempt1();
let url2: Result<string, Error2> = attempt2();
let url3: Result<string, Error3> = attempt3();

let result = Result.any(url1, url2, url3); // Result<string, Error1 | Error2 | Error3>

let url = result.unwrap(); // At least one attempt gave us a successful url

Usage with rxjs

resultMap

Allows you to do the same actions as the normal rxjs map operator on a stream of Result objects.

import { of, Observable } from "rxjs";
import { Ok, Err, Result } from "ts-results";
import { resultMap } from "ts-results/rxjs-operators";

const obs$: Observable<Result<number, Error>> = of(Ok(5), Err("uh oh"));

const greaterThanZero = obs$.pipe(
  resultMap((number) => number > 0), // Doubles the value
); // Has type Observable<Result<boolean, 'uh oh'>>

greaterThanZero.subscribe((result) => {
  if (result.ok) {
    console.log("Was greater than zero: " + result.val);
  } else {
    console.log("Got Error Message: " + result.val);
  }
});

// Logs the following:
// Got number: 10
// Got Error Message: uh oh

resultMapErr

import { resultMapErr } from "ts-results/rxjs-operators";

Behaves exactly the same as resultMap, but maps the error value.

resultMapTo

import { resultMapTo } from "ts-results/rxjs-operators";

Behaves the same as resultMap, but takes a value instead of a function.

resultMapErrTo

import { resultMapErrTo } from "ts-results/rxjs-operators";

Behaves the same as resultMapErr, but takes a value instead of a function.

elseMap

Allows you to turn a stream of Result objects into a stream of values, transforming any errors into a value.

Similar to calling the else function, but works on a stream of Result objects.

import { of, Observable } from "rxjs";
import { Ok, Err, Result } from "ts-results";
import { elseMap } from "ts-results/rxjs-operators";

const obs$: Observable<Result<number, Error>> = of(
  Ok(5),
  Err(new Error("uh oh")),
);

const doubled = obs$.pipe(
  elseMap((err) => {
    console.log("Got error: " + err.message);

    return -1;
  }),
); // Has type Observable<number>

doubled.subscribe((number) => {
  console.log("Got number: " + number);
});

// Logs the following:
// Got number: 5
// Got error: uh oh
// Got number: -1

elseMapTo

import { elseMapTo } from "ts-results/rxjs-operators";

Behaves the same as elseMap, but takes a value instead of a function.

resultSwitchMap and resultMergeMap

Allows you to do the same actions as the normal rxjs switchMap and rxjs switchMap operator on a stream of Result objects.

Merging or switching from a stream of Result<T, E> objects onto a stream of <T2> objects turns the stream into a stream of Result<T2, E> objects.

Merging or switching from a stream of Result<T, E> objects onto a stream of Result<T2, E2> objects turn the stream into a stream of Result<T2, E | T2> objects.

import { of, Observable } from "rxjs";
import { Ok, Err, Result } from "ts-results";
import { resultMergeMap } from "ts-results/rxjs-operators";

const obs$: Observable<Result<number, Error>> = of(
  new Ok(5),
  new Err(new Error("uh oh")),
);

const obs2$: Observable<Result<string, CustomError>> = of(
  new Ok("hi"),
  new Err(new CustomError("custom error")),
);

const test$ = obs$.pipe(
  resultMergeMap((number) => {
    console.log("Got number: " + number);

    return obs2$;
  }),
); // Has type Observable<Result<string, CustomError | Error>>

test$.subscribe((result) => {
  if (result.ok) {
    console.log("Got string: " + result.val);
  } else {
    console.log("Got error: " + result.val.message);
  }
});

// Logs the following:
// Got number: 5
// Got string: hi
// Got error: custom error
// Got error: uh oh

filterResultOk

Converts an Observable<Result<T, E>> to an Observble<T> by filtering out the Errs and mapping to the Ok values.

import { of, Observable } from "rxjs";
import { Ok, Err, Result } from "ts-results";
import { filterResultOk } from "ts-results/rxjs-operators";

const obs$: Observable<Result<number, Error>> = of(
  new Ok(5),
  new Err(new Error("uh oh")),
);

const test$ = obs$.pipe(filterResultOk()); // Has type Observable<number>

test$.subscribe((result) => {
  console.log("Got number: " + result);
});

// Logs the following:
// Got number: 5

filterResultErr

Converts an Observable<Result<T, E>> to an Observble<T> by filtering out the Oks and mapping to the error values.

import { of, Observable } from "rxjs";
import { Ok, Err, Result } from "ts-results";
import { filterResultOk } from "ts-results/rxjs-operators";

const obs$: Observable<Result<number, Error>> = of(
  new Ok(5),
  new Err(new Error("uh oh")),
);

const test$ = obs$.pipe(filterResultOk()); // Has type Observable<number>

test$.subscribe((result) => {
  console.log("Got number: " + result);
});

// Logs the following:
// Got number: 5
0.1.1

1 year ago

0.1.0

1 year ago