dismatchdismatch
Standalone API

Folding

fold — exhaustive single-pass aggregation over a collection, and foldWithDefault — partial aggregation with a fallback.

fold and foldWithDefault aggregate a collection of discriminated union values into a single accumulator. They replace the reduce + switch (or reduce + match + .exhaustive()) pattern with a purpose-built primitive.

fold

Exhaustive single-pass aggregator. Each handler receives (accumulator, variantData) and returns the new accumulator. All variants must have handlers.

import { fold } from "dismatch";

type Shape =
  | { type: "circle"; radius: number }
  | { type: "rectangle"; width: number; height: number };

const shapes: Shape[] = [
  { type: "circle", radius: 5 },
  { type: "rectangle", width: 4, height: 6 },
  { type: "circle", radius: 10 },
];

const stats = fold(shapes, { circles: 0, totalArea: 0 })({
  circle: (acc, { radius }) => ({
    circles: acc.circles + 1,
    totalArea: acc.totalArea + Math.PI * radius ** 2,
  }),
  rectangle: (acc, { width, height }) => ({
    ...acc,
    totalArea: acc.totalArea + width * height,
  }),
});
Try in Playground

Why fold instead of reduce

Compare the same aggregation in plain TypeScript:

const stats = shapes.reduce(
  (acc, shape) => {
    switch (shape.type) {
      case "circle":
        return {
          circles: acc.circles + 1,
          totalArea: acc.totalArea + Math.PI * shape.radius ** 2,
        };
      case "rectangle":
        return {
          ...acc,
          totalArea: acc.totalArea + shape.width * shape.height,
        };
    }
  },
  { circles: 0, totalArea: 0 },
);

It works, but it's not exhaustive — adding a new variant produces no compile error, and the implicit undefined fall-through is a runtime hazard. With fold, missing a variant is a compile error and the accumulator type is declared once.

foldWithDefault

Partial aggregation with a required Default fallback. Unhandled variants route to Default, which receives (accumulator, fullItem) so you can inspect the discriminant and branch inside.

import { foldWithDefault } from "dismatch";

type Notification =
  | { type: "push"; urgent: boolean; message: string }
  | { type: "email"; subject: string }
  | { type: "sms"; from: string };

const urgentCount = foldWithDefault(notifications, 0)({
  push: (acc, { urgent }) => acc + (urgent ? 1 : 0),
  Default: (acc) => acc, // email and sms fall through here
});
Try in Playground

Branching inside Default

const log = foldWithDefault(notifications, [] as string[])({
  push: (acc, { message }) => [...acc, `[PUSH] ${message}`],
  Default: (acc, item) => {
    if (item.type === "email") return [...acc, `[EMAIL] ${item.subject}`];
    return acc; // sms silently skipped
  },
});

foldWithDefault never throws UnknownVariantError.

Reach for what next

On this page