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 🙏

© 2025 – Pkg Stats / Ryan Hefner

ts-decider

v1.0.4

Published

TypeScript utilities for implementing the decider pattern

Readme

ts-decider

A minimal, type-safe utility for modeling domain logic using the decider pattern. This pattern is particularly suitable for event-sourced and functional architectures, where domain behavior is expressed as the result of handling commands and applying events.


Installation

npm install ts-decider

Overview

This library encourages structuring business logic as the composition of two pure functions:

  • decide: maps a command and current state to a list of domain events
  • evolve: applies a single event to an aggregate to produce a new state

Failures are modeled explicitly using structured failure events. The focus is on the behavior of your domain, not on parsing, validation, or infrastructure concerns.


Example: Task Management

import { v4 as uuid } from "uuid";
import { Decider, deciderHelpers, FailEvt } from "ts-decider";

// 1 - model your domain object using Types

type Task = {
  id: string;
  title: string;
  completed: boolean;
  createdAt: number;
  completedAt?: number;
};

// 2- model commands and events describing all state transitions

type TaskCmd =
  | { type: "create-task"; data: { title: string } }
  | { type: "complete-task"; data: { id: string } }
  | { type: "reopen-task"; data: { id: string } };

type TaskEvt =
  | { type: "task-created"; data: Task }
  | { type: "task-completed"; data: { id: string; completedAt: number } }
  | { type: "task-reopened"; data: { id: string } };

//  3 - specify what can go wrong with Failures (optional)

//  type TaskFailures = "cannot_reopen_task:task_is_not_completed" | "cannot_complete_task:task_is_already_completed";

// 4 - create a Decider type alias

type TaskDecider = Decider<Task, TaskCmd, TaskEvt, TaskFailures>;

// 5 - generate the Helpers injecting the right types using your decider alias

const h: TaskDecider["helpers"] = deciderHelpers<
  TaskDecider["state"],
  TaskDecider["cmds"],
  TaskDecider["evts"],
  //Optional: TaskDecider["fails"]
>();

// 6 - implement a Decide function

const decide: TaskDecider["decide"] = (cmd) => (task) => {
  switch (cmd.type) {
    case "create-task":
      return [{
        type: "task-created",
        data: {
          id: uuid(),
          title: cmd.data.title,
          completed: false,
          createdAt: Date.now()
        }
      }];
    case "complete-task":
      if (!task) return [h.failEvt(cmd)("state_is_undefined")];
      return [{
        type: "task-completed",
        data: {
          id: cmd.data.id,
          completedAt: Date.now()
        }
      }];
    case "reopen-task":
      if (!task) return [h.failEvt(cmd)("state_is_undefined")];
      return [{
        type: "task-reopened",
        data: {
          id: cmd.data.id
        }
      }];
    default:
      return [h.failEvt(cmd)("command_not_found")];
  }
};

// 7 - implement an Evolve function

const evolve: TaskDecider["evolve"] = (task) => (evt) => {
  switch (evt.type) {
    case "task-created":
      return evt.data;
    case "task-completed":
      return task ? { ...task, completed: true, completedAt: evt.data.completedAt } : task;
    case "task-reopened":
      return task ? { ...task, completed: false, completedAt: undefined } : task;
    case "operation-failed":
      return task;
  }
};

// 8 - apply the decide and evolve functions to the Run helper

export const tasksDecider: TaskDecider["run"] = h.run(decide)(evolve);

// 9 - Use it!

const createResult = tasksDecider({ type: "create-task", data: { title: "Write documentation" } })(undefined);
const completeResult = tasksDecider({ type: "complete-task", data: { id: 'abc' } })(createResult.state);
const failedResult = tasksDecider({ type: "complete-task", data: { id: 'abc' } })(undefined);

console.log('create example:', createResult);
console.log('complete example:', completeResult);
console.log('failed operation example:', failedResult);

/*
create example:
{
  state: {
    id: 'abc',
    title: 'Write documentation',
    completed: false,
    createdAt: 1720000000000
  },
  evts: [
    {
      type: 'task-created',
      data: {
        id: 'abc',
        title: 'Write documentation',
        completed: false,
        createdAt: 1720000000000
      }
    }
  ]
}

complete example:
{
  state: {
    id: 'abc',
    title: 'Write documentation',
    completed: true,
    createdAt: 1720000000000,
    completedAt: 1720000001000
  },
  evts: [
    {
      type: 'task-completed',
      data: {
        id: 'abc',
        completedAt: 1720000001000
      }
    }
  ]
}

failed operation example:
{
  state: undefined,
  evts: [
    {
      type: 'operation-failed',
      data: {
        cmd: { type: 'complete-task', data: { id: 'abc' } },
        reason: 'state_is_undefined'
      }
    }
  ]
}
*/

API

Decider<A, C, E, F>

type Decider<A, C, E, F> = {
  agg: A | undefined;
  cmd: C;
  evt: E | FailEvt<C, F>;
  fails: F;
  decide: (cmd: C) => (agg: A | undefined) => Array<E | FailEvt<C, F>>;
  evolve: (agg: A | undefined) => (evt: E | FailEvt<C, F>) => A | undefined;
  run: (cmd: C) => (agg: A | undefined) => {
    state: A | undefined;
    evts: Array<E | FailEvt<C, F>>;
  };
  helpers: DeciderHelpers<A | undefined, C, E | FailEvt<C, F>, F>;
};

deciderHelpers<A, C, E, F>()

Returns:

  • failEvt(cmd)(reason): constructs a typed failure event
  • run(decide)(evolve)(cmd)(agg): executes the decision and state transition

Failure Modeling

When a command cannot be applied — due to missing state, an invalid transition, or an unrecognized input — ts-decider allows you to emit a structured failure event instead of throwing or branching imperatively.

Failures are expressed using a typed FailEvt:

type FailEvt<C, F extends string> = {
  type: "operation-failed";
  data: {
    cmd: C;
    reason: F;
  };
};

By default, ts-decider includes two predefined failure reasons:

type DefaultFails = "cmd_not_found" | "state_is_undefined";

You may extend this with your own application-specific reasons by parameterizing the decider with a custom string union:

type MyFailures = "permission_denied" | "quota_exceeded";

This allows failure handling code — whether in tests, outbox processors, or monitoring — to match on specific reasons using a switch or conditional:

if (evt.type === "operation-failed") {
  switch (evt.data.reason) {
    case "permission_denied":
      // do something
    case "state_is_undefined":
      // do something else
  }
}

Design Considerations

This code is intended to represent core domain logic. It assumes the following architectural constraints:

  • Data structure validation and parsing occur outside this layer, typically at the application boundary or transport layer (e.g., API layer, event deserializer).
  • Inputs are expected to be well-formed by the time they reach the decider.
  • This allows the domain logic to remain focused on business behavior rather than structural correctness.

This approach supports separation of concerns and aligns with architectural styles like functional core/imperative shell, clean architecture, and hexagonal architecture.


Developer Experience

Typing decide and evolve as YourDecider["decide"] and YourDecider["evolve"] provides:

  • Autocomplete for all valid cmd.type and evt.type cases in switch statements
  • Type narrowing for accessing cmd.data or evt.data
  • Compile-time safety for handling unexpected or incomplete logic
  • Better editor feedback when evolving your model or making changes

This reduces cognitive overhead and makes domain modeling easier to evolve safely.


Integration and the Outbox Pattern

The return value of run(cmd)(state) is an object with the final aggregate state and a list of events:

{
  state: A | undefined;
  evts: Array<E | FailEvt<C, F>>;
}

This structure lends itself naturally to outbox-based architectures, allowing flexibility in how you integrate with external systems:

  • You can persist the state and enqueue the events in a single transaction.
  • You can persist only the events (in fully event-sourced systems), or only the state (in state-based apps).
  • You can summarize the outcome before enqueueing notifications or API calls.

Author

Giovanni Chiodi github.com/mean-machine-gc