1.0.0 • Published 4 years ago

typed-result v1.0.0

Weekly downloads
2
License
MIT
Repository
github
Last release
4 years ago

typed-result

typed-result is a typesafe implementation in idiomatic TypeScript liberally adapted from its Rust counterpart and without external dependencies.

It is useful to safely handle code that can potentially fail.

See the full documentation.

Install

npm install typed-result

or

yarn add typed-result

Why is it useful?

typed-result promotes handling errors in a declarative way instead of with exceptions. It is an alternative to try/catch blocks, with a syntax closer to Promise and async/await (using generators).

Usage

Basics

typed-result provides wrapper/unwrappers to convert exception-style control flow to result-style control flow:

ok(1); // { kind: "Ok", value: 1 }
err(new Error()); // { kind: "Err", error: Error }

const divide = wrapFn((a: number, b: number) => {
  if(b === 0) {
    throw new Error("divide by 0");
  }
  return a / b;
});

divide(4, 2); // { kind: "Ok", value: 2 }
divide(1, 0); // { kind: Err", error: Error("divide by 0") }

isOk(divide(4, 2)); // true
isErr(divide(1, 0)); // false

unwrap(divide(4, 2)); // 2
unwrap(divide(1, 0)); // throws

unwrapFn(divide)(4, 2); // 2
unwrapFn(divide)(1, 0); // throws

Chainable container

A special class, _, is available as a wrapper to chain operations:

const divide = _.wrapFn((a: number, b: number) => {
  if(b === 0) {
    throw new Error("divide by 0");
  }
  return a / b;
});

divde(4, 2) // _.Ok(2)
  .mapOk(v => v * 3) // _.Ok(6)
  .mapErr(() => {
    throw new Error("unknown error");
  }) // _.Ok(6) (no-op)
  .mapOk(v => v + 2) // _.Ok(8)
  .unwrap(); // 6

divde(1, 0) // _.Err()
  .mapOk(v => v * 3) // _.Err() (no-op)
  .mapErr(() => {
    throw new Error("unknown error");
  }) // _.Err(Error("unknown error"))
  .mapOk(v => v + 2) // _.Err() (no-op)
  .unwrap(); // throws

divide(1, 0)
  .mapErr(() => {
    throw new Error("unknown error")
  }) // _.Err(Error("unknown error"))
  .unwrap(); // throws

Generators

To simplify the code flow, it is possible to use an async-await-like syntax to yield results and wrapped results:

unwrap(drain(function*() {
  const x = yield divide(100, 5); // 20
  const y = yield divide(x, 4); // 5
  return y + 1; // 6
})); // 6

unwrap(drain(function*() {
  const x  = yield divide(100, 5); // 20
  const y = yield divide(x, x - 20); // throws
  return y; // not reached
})); // throws

_.drain(function*() {  
  const x = yield divide(100, 5); // 20
  const y = yield divide(x, 4); // 5
  return y + 1; // 6
})
  .mapOk(n => n * 2)
  .unwrap(); // 12

_.drain(function*() {  
  const x = yield divide(100, 5); // 20
  const y = yield divide(x, x - 20); // throws
  return y; // not reached
})
  .mapOk(n => n * 2) // not called
  .mapErr(() => null) // _.Ok(null)
  .unwrap(); // null

Async and Promises

Some basic utilities are also provided to work with Promises / async-await - since it is very common to have async operations that may fail:

const fetchAndDivide = wrapAsyncFn(async (urlA: string, urlB: string) => {
  const [a, b] = await Promise.all([
    fetch(urlA).then(r => r.json()),
    fetch(urlB).then(r => r.json()),
  ]);
  if(b === 0) {
    throw new Error();
  }
  return a / b;
});

unwrap(await fetchAndDivide("/api/a", "/api/b"));

Async versions of drain and _.drain are also available, and they are very handy in this case:

await drainAsync(async function*() {
  const x = await yield fetchAndDivide("/api/a", "/api/b");
  const y = yield divide(x, 5);
  return y;
}).then(unwrap);

(await _.drainAsync(async function* () {
  const x = await yield fetchAndDivide("/api/a", "/api/b");
  const y = yield divide(x, 5);
  return y;
}))
  .mapOk(...)
  .mapErr(...)
  .unwrap();

Working with collections

Working with collections require no special utilities. Array and Result compose very well:

[1, 2, 5, 6, null, 0]
  .map(_.Ok) // [_.Ok(1), _.Ok(2), ...]
  .map(
    (r) =>
      r
        .mapOk((v) => {
          if (typeof v !== "number") {
            throw new TypeError();
          }
          return v;
        })
        .mapOk((v) => {
          if (v % 2 !== 0) {
            throw new Error();
          }
          return v;
        })
        .mapOk((v) => 2 * v).result,
  )
  .filter(isOk) // [_.Err(), _.Ok(4), _.Err(), _.Ok(12), ...]
  .map((r) => r.value) // [4, 12, 0]

Has it anything to do with "monads"?

Yes. No. Kind of. Maybe. Who cares?