About

CooperTS is a collection of Elm-inspired functional-programming tools in Typescript. CooperTS is focused on eliminating runtime exceptions and reducing testing burden.

Why CooperTS?

CooperTS lets the Typescript compiler do as much of the work as possible.

Avoid null Errors

CooperTS provides Maybe<T> to represent values that are nullable.

Example:

function find<T>(fn: (t: T) => boolean, ts: ReadonlyArray<T>): Maybe<T> {
  for (const t of ts) {
    if (fn(t)) {
      return just(t);
    }
  }
  return nothing<T>();
}

Handle Errors Without Raising Exceptions

CooperTS provides Result<E, T> to represent the result of a computation that can fail.

Example:

import { ok, err } from 'resulty';

export const decodeBase64: Result<string, string> = (value: string) => {
  try {
    const decodedStr = Base64.decode(value);
    return ok(decodedStr);
  } catch {
    return err(`Expected a base64 encoded string but got ${value}`);
  }
};

Verify Types at the App's Edges

CooperTS provides Decoder<T> for verifying the type of unknown objects or JSON and converting it into a shape the app can use. Rather than type-checking external data as it is used, Decoders verify data right when it is received.

Instead of:

export const helloUser = (data: string) => {
  const value: unknown = JSON.parse(data);
  if (
    value &&
    typeof value === 'object' &&
    'user' in value &&
    value.user &&
    typeof value.user === 'object' &&
    'name' in value.user &&
    typeof value.user.name === 'string'
  ) {
    console.log(`Hello ${value.user.name}`);
  }
};

Decoders let us write:

import { field, string } from 'jsonous';

export const helloUserWithDecoder = (data: string) => {
  field('user', field('name', string))
    .decodeJson(data)
    .map((name) => `Hello ${name}`)
    .do(console.log);
};

Prefer Pure Functions by Isolating Side-Effects

CooperTS provides Task<E, T> for wrapping side-effects. This is similar to JavaScript's Promise, except that Tasks do not run until they are forked, so a function can create a Task without causing any side-effects.

This allows us to create and run a Task in two separate places, which gives us the ability to build large chains of tasks from smaller tasks.

For example, this method creates a Task that is in charge of sending a Slack message, but no Slack message is sent until the Task is forked.

export const sendMessage = (event: Event) =>
  Task.succeed<ActionFailed, {}>({})
    .assign('zenQuote', getZenQuote)
    .assign('slackChannel', slackChannel)
    .assign('slackWebhookUrl', slackWebhookUrl)
    .andThen(postQuoteToSlack(event));

Prefer Functional Programming Over Nesting Logic

Maybe<T>, Result<E, T>, Decoder<T>, and Task<E, T> all have similar methods, and can be used to avoid complex if/else scenarios.

For example, we can use the find method from the section above on avoiding null errors to simplify this code:

const users: Array<{ id: number; parentId: number; name: string }> = getUsers();
const user = users.find(({ id }) => id === 123);

if (user) {
  const parent = users.find(({ id }) => id === user.parentId);
  if (parent) {
    console.log(`Found parent of user #123: ${parent.name}!`);
  }
}

Into this:

const users: Array<{ id: number; parentId: number }> = findUsers();

find(({ id }) => id === 123, users)
  .andThen(({ parentId }) => find(({ id }) => id === parentId, users))
  .map(({ name }) => `Found parent of user #123: ${name}!`)
  .do(console.log);