npm package: @mobile-club/upshot
Description: # Upshot ☯
Author: Mobile Club
Latest version: 0.4.0

## ⚠️ Documentation is WIP

# Upshot ☯

> **Upshot** - _noun_ / the final or eventual outcome or conclusion of a discussion, action, or series of events.

## Description

When consuming APIs such as `JSON.parse: any` or `findUser(): Promise<User>`, we make the false assumption that they are not going to throw (as per their signature), and
even if they do, we don't know what type of errors it's going to yield. It's even more true when consuming third-party libraries. This leads to unsafe code with uncaught exceptions all over the place.

In order to write better and safer software, we can leverage a well-known tactical pattern to better handle errors or apply and chain computations on "eventual" values or errors.

Let's first note that not ALL program exceptions should be avoided. We divide errors into 2 types :

- **Systemic errors**: Out-of-memory errors, no more space on disk, maybe even some infrastructure errors (db down), etc
- **Application errors**: User with specified id was not found, A post can't be liked twice by a user, A banned user cannot do action X, etc.

Systemic errors are often not recoverable, and it's fine (or wanted) if an exception bubbles up your stack and triggers the red bell of your monitoring systems.

Application errors are part of the life of your system. They are expected and thus should be treated as any other value, they should be explicitly exposed by the underlying APIs that could return them (in their signature), so
that they can be handled in place instead of leaking uncaught exceptions up to the root stack.

---

This library exposes `Upshot<E, R>` data type (also known as `Either` or `Result` in other systems) and is defined as the following :

```typescript
type Upshot<E, R> = Ko<E> | Ok<R>;
```

It provides a set of functions to apply computations over this data type.

All the functions :

- Provide both curried and uncurried signatures
- Work with both sync and async operations (no dedicated data structures to work with async code)

The goals of the library are :

- As much minimal as possible (do not go beyond the scope of error management)
- Easy to use (compact features with polymorphic return types)
- Provide a good DX (Expose the same APIs for both sync & async operations)

## Table of contents

## Installation

⚠️ Package is not published yet

```bash
yarn add @mobile-club/upshot
```

## Test

```bash
yarn test
```

## Documentation

### `ok`

Wraps a value `T` into an `Upshot<never, T>`

```typescript
import { ok } from "@mobile-club/upshot";

const x = ok(42); // Upshot<never, 42>
```

### `ko`

Wraps a value `E` into an `Upshot<E, never>`

```typescript
import { ko } from "@mobile-club/upshot";

const x = ko(42); // Upshot<42, never>
```

### `isOk`

Checks if an `Upshot<any, R>` can be narrowed to `Ok<R>`

```typescript
import { ok } from "@mobile-club/upshot";

const x = ok(42); // Upshot<never, 42>

if (isOk(x)) {
  x; // Ok<42>
  x.value; // 42
}
```

### `isKo`

Checks if an `Upshot<E, any>` can be narrowed to `Ko<E>`

```typescript
import { ko } from "@mobile-club/upshot";

const x = ko(42); // Upshot<42, never>

if (isOk(x)) {
  x; // Ko<42>
  x.value; // 42
}
```

### `isUpshot`

Checks if a value is an `AnyUpshot`

```typescript
import { isUpshot } from "@mobile-club/upshot";

const x = // ...;

if (isUpshot(x)) {
  x       // AnyUpshot
  x.value // any
}
```

### `pipe`

`pipe` does not deal specifically with `Upshot`, it's a utility function we use very often in order to compose multiple functions together.
`pipe` takes a variadic amount of parameters. The first parameter is a value, the rest parameter is a list of functions.
The first function will be called with the value as a parameter, the second function will be called with the result of the previous computation as a parameter, and so on...

We can pass both sync and async functions. If at least **one** function is async, the result with always be async (Promise).

```typescript
const x1 = pipe(42, (x) => x + 1); // 43

const x2 = pipe(
  42,
  (x) => x + 1,
  (x) => x + 1
); // 44

const x3 = pipe(
  42,
  (x) => x + 1,
  async (x) => x + 1
); // Promise<44>

const x4 = pipe(
  42,
  async (x) => x + 1,
  (x) => x + 1
); // Promise<44>
```

Why not use an already existing `ramda` pipe for instance? because it does not work when mixing sync and async functions out of the box (without using `R.andThen`) :

```typescript
// ramda
const x = pipe(
  42,
  (x) => x + 1,
  (x) => x + 1
); // 44

// upshot (same)
const x = pipe(
  42,
  (x) => x + 1,
  (x) => x + 1
); // 44
```

when using async :

```typescript
// ramda
const x = pipe(
  42,
  async (x) => x + 1,
  andThen((x) => x + 1)
); // Promise<44>

// upshot
const x = pipe(
  42,
  async (x) => x + 1,
  (x) => x + 1
); // Promise<44>
```

### `mapOk`

Apply function to an upshot only if `isOk` and returns a new `Upshot` based on the function's return type. It returns
identity otherwise.

```typescript
import { ok, ko, mapOk } from "@mobile-club/upshot";

const addOne = (x) => x + 1;
const addOneAsync = async (x) => x + 1;

const x = ok(42);               // Upshot<never, 42>
const x2 = mapOk(x, addOne);      // Upshot<never, 43>
const x3 = mapOk(x, addOneAsync); // Upshot<never, 43> | Promise<Upshot<never, 43>>

const y = ko(42);               // Upshot<42, never>
const y2 = mapOk(y, addOne);      // Upshot<42, never>
```

As you can see in the above example, `mapOk()` takes a function that takes the unwrapped value, and returns a new value (+
1). But what if the function returns a new `Upshot`? It works exactly the same. Upshots get automatically flattened,
meaning that :

```typescript
import { ok, mapOk } from "@mobile-club/upshot";

const x1 = mapOk(ok(42), () => 43);           // Upshot<never, 43>
const x2 = mapOk(ok(42), () => ok(43));       // Upshot<never, 43>
const x3 = mapOk(ok(42), () => ko(43));       // Upshot<43, never>

const x4 = mapOk(ok(42), async () => 43);     // Upshot<never, 43> | Promise<Upshot<never, 43>>
const x5 = mapOk(ok(42), async () => ko(43)); // Upshot<43, never> | Promise<Upshot<43, never>>
```

Chaining computations with `mapOk` will also merge error types :

```typescript
import { Upshot, mapOk, pipe } from "@mobile-club/upshot";

declare const findUser: (id: number) => Promise<Upshot<"USER_NOT_FOUND", User>>;
declare const makeUserAdmin: (
  user: User
) => Promise<Upshot<"USER_ALREADY_ADMIN", AdminUser>>;

const result = await findUser(42).then(mapOk(makeUserAdmin)); // Upshot<"USER_NOT_FOUND" | "USER_ALREADY_ADMIN", AdminUser>

// or

const result = await pipe(findUser(42), mapOk(makeUserAdmin)); // Upshot<"USER_NOT_FOUND" | "USER_ALREADY_ADMIN", AdminUser>

// or

const user = await findUser(42);               // Upshot<"USER_NOT_FOUND", User>
const result = await mapOk(user, makeUserAdmin); // Upshot<"USER_NOT_FOUND" | "USER_ALREADY_ADMIN", AdminUser>
```

### `mapKo`

Apply function to an upshot only if `isKo` and returns a new `Upshot` based on the function's return type. It
returns `identity` otherwise.

```typescript
import { ok, ko, mapOk } from "@mobile-club/upshot";

const addOne = (x) => x + 1;

const x = ok(42); // Upshot<never, 42>
const y = ko(42); // Upshot<42, never>

const x2 = mapKo(x, addOne); // Upshot<never, 42>
const y2 = mapKo(x, addOne); // Upshot<43, never>
```

Chaining computations with `mapKo` provides the capability to :

- Override an error with another

```typescript
const x1 = ko(42);                  // Upshot<42, never>
const x2 = mapKo(x1, () => 43);     // Upshot<43, never>
const x3 = mapKo(x1, () => ko(43)); // Upshot<43, never>
```

- Recover from an error

```typescript
const x1 = ko(42);                  // Upshot<42, never>
const x2 = mapKo(x1, () => ok(43)); // Upshot<never, 43>
```

### `fold`

Matches against `Upshot<E, R>` and call the associated function with the unwrapped value.

- If it's `Ok<R>` it will call the given `ok` function with value `R` as a parameter
- If it's `Ko<E>` it will call the given `ko` function with value `E` as a parameter

```typescript
import { fold, Upshot } from "@mobile-club/upshot";

declare const x: Upshot<"loose", "win">;

fold(x, {
  ok: (x) => `You ${x}`,
  ko: (x) => `You ${x}`,
}); // "You loose" | "You win"
```

### `tap`

Matches against `Upshot<E, R>` and call the associated function with the unwrapped value. Unlike `fold` the returned value
is ignored. A good use case is typically logging.

```typescript
import { tap, Upshot } from "@mobile-club/upshot";

declare const x: Upshot<"loose", "win">;

tap(x, {
  ok: (x) => console.log(`You ${x}`),
  ko: (x) => console.log(`You ${x}`),
}); // Upshot<"loose", "win">

// Will print either "You loose" or "You win". Upshot remains unchanged.
```

The code above is equivalent to 


```typescript
import { ok, fold, Upshot } from "@mobile-club/upshot";

declare const x: Upshot<"loose", "win">;

fold(x, {
  ok: (x) => {
    console.log(`You ${x}`);
    
    return ok(x)
  },
  ko: (x) => {
    console.log(`You ${x}`);
    
    return ko(x);
  },
});
```


### `getOrElse`

Unwraps upshot. If it's an `Ok<R>`, it will return the underlying value. If it's a `Ko<E>`, it will return the default
value

```typescript
import { ok, ko, getOrElse } from "@mobile-club/upshot";

const x = getOrElse(ok(42), 43); // 42;
const y = getOrElse(ko(42), 43); // 43
```

### `option`

Wraps an optional (`null` or `undefined`) value `R` into an `Upshot<E, NonNullable<R>>` where `E` is this error type for when `R` is `null` or `undefined`

```typescript
import { option } from "@mobile-club/upshot";

declare const user: User | undefined | null;

const x1 = option(user, "Some Error");      // Upshot<"Some Error", User>;
const x2 = option("Some Error")(user);      // Upshot<"Some Error", User>;

const x3 = option(null, "Some Error");      // Upshot<"Some Error", never>;
const x4 = option(undefined, "Some Error"); // Upshot<"Some Error", never>;
const x5 = option(42, "Some Error");        // Upshot<"Some Error", 42>;
```

### `maybe`

Shorthand for `option(undefined)` which wraps value `R` into an `Upshot<Nothing, NonNullable<R>>`.

This can be compared to the type `Maybe<A>` in other languages/libraries where `Maybe<A> = A | Nothing`

```typescript
import { maybe } from "@mobile-club/upshot";

declare const user: User | undefined | null;

const x1 = maybe(user);      // Upshot<Nothing, User>;
const x2 = maybe(null);      // Upshot<Nothing, never>;
const x3 = maybe(undefined); // Upshot<Nothing, never>;
const x4 = maybe(42);        // Upshot<Nothing, 42>;
```

### `merge`

Takes a list of `AnyUpshot` and merges it into a single `Upshot`. It will concat error and success types.
If there's one `Ko` in the list, the resulting type will always be `Ko`.

#### signature

```typescript
import { merge } from "@mobile-club/upshot";

merge([ok(45), ok(44), ko(43), ko(42)]); // Upshot<Array<42 | 43>, [44, 45]>
merge([ok(45), ok(44)]);                 // Upshot<never, [44, 45]>
```

### `all`

Same as `merge`, but it will retain only the first `Ko<E>` instead of concatenating errors

```typescript
import { all } from "@mobile-club/upshot";

all([ok(45), ok(44), ko(43), ko(42)]); // Upshot<42 | 43, [44, 45]>
all([ok(45), ok(44)]);                 // Upshot<never, [44, 45]>
```

### `unsafeValue`

Unwraps an `Upshot`.

- If it's `Ok<R>`, returns underlying value `R`
- If it's `Ko<E>`, throws underlying value `E`

```typescript
import { unsafeValue } from "@mobile-club/upshot";

unsafeValue(ok(42)); // 42
unsafeValue(ko(42)); // will throw 42
```

### `safe`

Allows to write throwable code that is caught and mapped to an `Upshot`

```typescript
import { safe } from "@mobile-club/upshot";

safe({
  try: () => JSON.parse("<"),
}); // Upshot<unkown, any>

safe({
  try: () => JSON.parse("<"),
  catch: (error /* unknown*/) => "PARSING_ERROR",
}); // Upshot<"PARSING_ERROR", any>

declare const findUser: () => User;

safe({
  try: findUser,
}); // Upshot<unkown, User>

safe({
  try: findUser,
  catch: (error /* unknown*/) => "FIND_USER_ERROR",
}); // Upshot<"FIND_USER_ERROR", User>

declare const findUserAsync: () => Promise<User>;

safe({
  try: findUserAsync,
}); // Promise<Upshot<unkown, User>>

safe({
  try: findUserAsync,
  catch: (error /* unknown*/) => "FIND_USER_ERROR",
}); // Promise<Upshot<"FIND_USER_ERROR", User>>

declare const findUserUpshot: () => Upshot<"USER_NOT_FOUND", User>;

safe({
  try: findUserUpshot,
}); // Promise<Upshot<unkown, User>>

safe({
  try: findUserUpshot,
  catch: (error /* unknown*/) => "UNKNOWN_ERROR",
}); // Promise<Upshot<"FIND_USER_ERROR" | "UNKNOWN_ERROR", User>>
```

As you can see, when `catch` is not provided, the resulting error type will **always** be `unknown` (`Upshot<unknown, R>`). It's because we cannot actually infer which type will be the error if an actual exception is thrown in the `try`.

Even for `declare const findUserUpshot: () => Upshot<"USER_NOT_FOUND", User>`, we know that it can return `USER_NOT_FOUND`, but if the method throws? then the error would be `USER_NOT_FOUND | unknown`, which typescript will infer narrow to `unknown`.

### sequence

`sequence` provide an imperative style mechanism to work with `Upshot`.

It leverages generators in order to interrupt & early return when a `Ko<E>` is encountered during a computation.

```typescript
import { sequence } from "@mobile-club/upshot";

declare const findUser: () => Upshot<"USER_NOT_FOUND", User>;
declare const makeUserAdmin: (
  user: User
) => Upshot<"USER_ALREADY_ADMIN", AdminUser>;

sequence(function* () {
  const user = yield* findUser();
  const admin = yield* makeUserAdmin(user);

  return admin;
}); // Upshot<"USER_NOT_FOUND" | "USER_ALREADY_ADMIN", AdminUser>
```

`sequence` also allows generators to be async

```typescript
import { sequence } from "@mobile-club/upshot";

declare const findUser: () => Promise<Upshot<"USER_NOT_FOUND", User>>;
declare const makeUserAdmin: (
  user: User
) => Promise<Upshot<"USER_ALREADY_ADMIN", AdminUser>>;

sequence(async function* () {
  const user = yield* await findUser();
  const admin = yield* await makeUserAdmin(user);

  return admin;
}); // Promise<Upshot<"USER_NOT_FOUND" | "USER_ALREADY_ADMIN", AdminUser>>
```

The `sequence` is convenient if you prefer to reason in a more imperative style but is also very useful when you want to
chain computation that depends on previous computations, and would like to return an aggregate of those computations.

Let's see an example.

Those 2 functions exist:

```typescript
// Let's say we have these two functions
declare const findUser: (id: number) => Upshot<Error, User>;
declare const findUserPosts: (user: User) => Upshot<Error, Post[]>;
```

And we need to implement the following function:

```typescript
type FindUserWithTotalUpvotes = (
  id: number
) => Upshot<Error, { user: User; totalUpvotes: number }>;
```

Naturally we would first think of chaining computation with `mapOk` :

```typescript
import { pipe, mapOk } from "@mobile-club/upshot";

const findUserWithTotalUpvotes: FindUserWithTotalUpvotes = (id: number) => pipe(
  findUser(id),
  mapOk(findUserPosts),
  mapOk(posts => {
    // Problem here, we only have access to posts (Post[]) in the current scope.
    // We don't have access to the user (User)
    return {
      user: ???
      totalUpvotes: posts.mapOk(...)
    }
  })
)
```

`findUserPosts` depends on `findUser` because it needs a `User`, altought we need both `User` and `Post[]` to compute the
return value.

We will need to do it in one `mapOk` :

```typescript
import { pipe, mapOk, isKo } from "@mobile-club/upshot";

const findUserWithTotalUpvotes: FindUserWithTotalUpvotes = (id: number) => pipe(
  findUser(id),
  mapOk(user => {
    const posts = findUserPosts(user); // Upshot<Error, Post[]>;

    // We have to check if previous function was Ko and early return
    if (isKo(posts)) {
      return posts;
    }

    return {
      user,
      totalUpvotes: posts.value.map(...)
    }
  })
)
```

Instead, we can leverage `sequence` so that the code has a nicer flow.

```typescript
import { sequence } from "@mobile-club/upshot";

const findUserWithTotalUpvotes: FindUserWithTotalUpvotes = (id: number) => sequence(function* () {
  const user = yield* findUser(id); // User
  const posts = yield* findUserPosts(user); // Post[]

  return {
    user,
    totalUpvotes: posts.map(...)
  }
})
```

⚠️ Generators cannot be defined as arrow functions, thus they have their own binding to `this`.

```typescript
class Foo {
  method1 = () => "bar";

  method2 = () =>
    sequence(function* () {
      // ...
      this.method1(); // This won't work. The compiler will yield the error : "TS2683: 'this' implicitly has type 'any' because it does not have a type annotation."
    });
}
```

You need to explicitly pass this and type it properly in order to work.

```typescript
class Foo {
  method1 = () => "bar";

  method2 = () =>
    sequence(function* (this: Foo) { // <-- annotate `this` type here
      // ...
      this.method1(); // ✅
    }, this); // <-- pass `this` value here.
}
```