dismatchdismatch

Removing dismatch

Plain-object output and TS-native types make removing dismatch a mechanical rewrite, not a redesign. Plus how to mix dismatch with switch / ts-pattern incrementally.

Lock-in is a fair concern when a library shows up at hundreds of call sites. dismatch is designed so removal is mechanical, not redesign:

  • Values are plain objects. circle(5) returns { type: "circle", radius: 5 }. Nothing to unwrap, nothing to migrate at the call sites that consume the data.
  • Types are TS-native discriminated unions. If you used the exit-friendly style, the type alias has no dependency on dismatch at all.
  • Matchers are exhaustive switch in disguise. TypeScript already enforces exhaustiveness on switch over discriminated unions.

Mechanical rewrite

Starting from this dismatch code:

import { createUnion } from "dismatch";

const _Result = createUnion({
  ok: (data: string) => ({ data }),
  err: (message: string) => ({ message }),
});
export const { ok, err, match, is } = _Result;
export type Result = ReturnType<typeof ok> | ReturnType<typeof err>;

// call sites
const r = ok("hi");
const banner = match({
  ok: ({ data }) => `Loaded: ${data}`,
  err: ({ message }) => `Error: ${message}`,
});
if (is("ok")(r)) r.data;

Replace constructors with object literals, matchers with switch, and is(...) with ===. The Result type alias does not change:

export const ok = (data: string) => ({ type: "ok" as const, data });
export const err = (message: string) => ({ type: "err" as const, message });
export type Result = ReturnType<typeof ok> | ReturnType<typeof err>;

const banner = (r: Result) => {
  switch (r.type) {
    case "ok":  return `Loaded: ${r.data}`;
    case "err": return `Error: ${r.message}`;
  }
};
const r = ok("hi");
if (r.type === "ok") r.data;

The constructor-call shape (ok("hi")) is unchanged at every call site — only the constructor definition file is touched. Matcher rewrites are local to wherever you called match(...).

Incremental adoption — you don't have to leave to mix tools

Because output is plain { type, ... }, dismatch values feed any other matcher unchanged. Use whichever tool fits the call site:

import { match as tsPatternMatch } from "ts-pattern";

const r = ok("hi"); // dismatch constructor

// Plain switch — zero dependencies
switch (r.type) {
  case "ok":  /* ... */ break;
  case "err": /* ... */ break;
}

// ts-pattern — for nested patterns, regex, wildcards
const label = tsPatternMatch(r)
  .with({ type: "ok" }, ({ data }) => data)
  .with({ type: "err" }, ({ message }) => message)
  .exhaustive();

// dismatch — for reusable typed matchers
const banner = match({
  ok:  ({ data })    => `Loaded: ${data}`,
  err: ({ message }) => `Error: ${message}`,
});

You can adopt createUnion for value generation today and reach for ts-pattern (or a plain switch) wherever its strengths matter — no conversion layer, no wrapping, no factory dependency at the matcher.

Why this works

  • createUnion constructors are thin functions that spread your data and inject the discriminant. The output is structurally identical to a hand- written tagged object.
  • Bound matchers (Result.match, Result.is, …) are the same standalone functions described in the Standalone API, pre-bound to the union type and discriminant. They never wrap the value.
  • The type system contribution is InferUnion — a thin alias over ReturnType of the schema's constructors. The exit-friendly style skips even that, deriving the type from ReturnType<typeof ok> | ... directly.

See also

On this page