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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mobile-club/upshot

v0.4.0

Published

# Upshot ☯

Downloads

111

Readme

⚠️ 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 :

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

yarn add @mobile-club/upshot

Test

yarn test

Documentation

ok

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

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

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

ko

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

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>

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>

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

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).

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) :

// 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 :

// 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.

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 :

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 :

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.

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
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
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
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.

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

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

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

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

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

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

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
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

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.

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

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:

// 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:

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

Naturally we would first think of chaining computation with mapOk :

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 :

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.

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.

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.

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

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