Skip to content

Snowflyt/tinyeffect

Repository files navigation

tinyeffect

Algebraic effects, in TypeScript.

Handle side effects in a unified way, with type-safety and elegance.

npm version minzipped size test status coverage status MIT license

screenshot

About

Programming heavily relies on side effects. Imagine a program without I/O capabilities (even basic console access) — it would be pretty useless.

There are many kinds of side effects: I/O operations, error handling, dependency injection, asynchronous operations, logging, and so on.

However, some effects don’t fit well with TypeScript’s type system — for example, error handling, where try-catch blocks only capture unknown types. Other effects, such as asynchronous operations, come with inherent challenges like asynchronous contagion.

That’s where tinyeffect comes in.

tinyeffect provides a unified way to handle all these side effects in a type-safe manner. It’s a tiny yet powerful library with its core logic implemented in only around 400 lines of code. The idea is inspired by the effect system from the Koka language. It uses algebraic effects to model side effects, which are then handled by effect handlers.

Don’t worry if you are not familiar with these concepts. tinyeffect is designed to be simple and easy to use. You can start using it right away without knowing the underlying theory. Simply start with the Usage section to see it in action.

Installation

npm install tinyeffect

Usage

Consider a simple example. Imagine a function that handles POST /api/users requests in a backend application. The function needs to:

  • Retrieve the current logged-in user from the context.
  • Check if the user has permission to create a new user (in this case, only admin users can create new users). If not, an error is thrown.
  • If the user has the necessary permission, create a new user in the database.

This example demonstrates three types of side effects: dependency injection (retrieving the current user), error handling (checking permissions), and asynchronous operations (database operations). Here’s how these side effects can be handled using tinyeffect and TypeScript:

import { dependency, effect, effected, error } from "tinyeffect";

type User = { id: number; name: string; role: "admin" | "user" };

const println = effect("println")<unknown[], void>;
const executeSQL = effect("executeSQL")<[sql: string, ...params: unknown[]], any>;
const askCurrentUser = dependency("currentUser")<User | null>;
const authenticationError = error("authentication");
const unauthorizedError = error("unauthorized");

const requiresAdmin = () => effected(function* () {
  const currentUser = yield* askCurrentUser();
  if (!currentUser) return yield* authenticationError();
  if (currentUser.role !== "admin")
    return yield* unauthorizedError(`User "${currentUser.name}" is not an admin`);
});

const createUser = (user: Omit<User, "id">) => effected(function* () {
  yield* requiresAdmin();
  const id = yield* executeSQL("INSERT INTO users (name) VALUES (?)", user.name);
  const savedUser: User = { id, ...user };
  yield* println("User created:", savedUser);
  return savedUser;
});

The code above defines five effects: println, executeSQL, currentUser, authentication, and unauthorized. Effects can be defined using effect, with dependency and error a