dismatchdismatch
Getting Started

Quick Start

Define a union, construct values, narrow types, pattern-match, and fold over collections — in five short steps.

This walk-through introduces the main APIs through a single running example. Each step builds on the previous one — by the end you will have used createUnion, is, match, matchWithDefault, map, mapAll, and fold.

1. Define a union

createUnion has two forms:

  • createUnion(schema) uses the default discriminant key "type".
  • createUnion(discriminant, schema) lets you pick a different key.

The discriminant is the property that holds the variant tag. In { type: "ok", data: "..." } the discriminant key is type and the active variant is "ok". Constructors return only the data fields — createUnion injects the discriminant automatically.

import { createUnion, type InferUnion } from "dismatch";

const Result = createUnion({
  ok: (data: string) => ({ data }),
  error: (message: string) => ({ message }),
  loading: () => ({}),
});

type Result = InferUnion<typeof Result>;
// { type: 'ok'; data: string }
// | { type: 'error'; message: string }
// | { type: 'loading' }
Try in Playground

If your data already uses a different key, pass it explicitly:

const Event = createUnion("kind", {
  click: (x: number) => ({ x }),
  key: (code: string) => ({ code }),
});

Re-exporting constructors

createUnion returns a plain object holding constructors, matchers, and metadata, so you can destructure and re-export anything you want. Two idioms cover the common cases.

Namespace style — keep the factory public, destructure for ergonomics:

// shape.ts
export const Shape = createUnion({
  circle: (radius: number) => ({ radius }),
  rectangle: (width: number, height: number) => ({ width, height }),
});
export type Shape = InferUnion<typeof Shape>;
export const { circle, rectangle } = Shape;

// consumer.ts
import { Shape, circle } from "./shape";
const c = circle(5);
const area = Shape.match({ /* ... */ });

Exit-friendly style — keep the factory file-private; only constructors and the type leave the module. The type alias derives from ReturnType of the constructors, so it has no dependency on the factory:

// shape.ts
const _Shape = createUnion({
  circle: (radius: number) => ({ radius }),
  rectangle: (width: number, height: number) => ({ width, height }),
});
export const { circle, rectangle, match, matchWithDefault, is } = _Shape;
export type Shape = ReturnType<typeof circle> | ReturnType<typeof rectangle>;

// consumer.ts
import { circle, match, type Shape } from "./shape";

The exit-friendly style is what makes Removing dismatch mechanical: the type alias survives even after the factory is gone, because it's a TS-native discriminated union.

2. Construct values

const r = Result.ok("hello"); // { type: 'ok', data: 'hello' }
const e = Result.error("fail"); // { type: 'error', message: 'fail' }
const l = Result.loading(); // { type: 'loading' }

3. Narrow with is

Use is(value, variant) inside if blocks for branch narrowing, and the bound Result.is(variant) form as a .filter() predicate factory. Both work with a single variant or an array of variants.

import { is } from "dismatch";

if (is(r, "ok")) {
  r.data; // narrowed to { type: 'ok'; data: string }
}

const errors = results.filter(Result.is("error"));
//    ^? { type: 'error'; message: string }[]

const settled = results.filter(Result.is(["ok", "error"]));
Try in Playground

Result.isKnown(value) is the runtime equivalent — true if the value is any declared variant. Use it at system boundaries (network responses, message queues) to validate incoming data.

4. Pattern-match

Every matcher is handlers-first and curried — pass the handlers and you get back a reusable function.

const label = Result.match({
  ok: ({ data }) => `Data: ${data}`,
  error: ({ message }) => `Error: ${message}`,
  loading: () => "Loading...",
});

label(r); // 'Data: hello'
results.map(label); // string[]
Try in Playground

Need to handle only some variants? Use matchWithDefault:

const banner = Result.matchWithDefault({
  error: ({ message }) => `Something went wrong: ${message}`,
  Default: () => "All good",
});
Try in Playground

To transform variants in place (preserving the discriminant), use map for partial and mapAll for exhaustive transforms:

const cleared = Result.map({
  error: ({ message }) => ({ message: "" }),
});

const normalized = Result.mapAll({
  ok: ({ data }) => ({ data: data.trim() }),
  error: ({ message }) => ({ message: message.trim() }),
  loading: () => ({}),
});
Try in Playground

5. Fold over a collection

fold aggregates a collection in a single exhaustive pass — no reduce wrapper, no switch.

const stats = Result.fold(results, { oks: 0, errors: 0, loadings: 0 })({
  ok: (acc) => ({ ...acc, oks: acc.oks + 1 }),
  error: (acc) => ({ ...acc, errors: acc.errors + 1 }),
  loading: (acc) => ({ ...acc, loadings: acc.loadings + 1 }),
});

Where next

On this page