createUnion
One schema, full toolkit — constructors, type guards, isKnown runtime check, and bound matchers, all derived from a single declaration.
createUnion turns a schema of variant constructors into a factory that
exposes everything you need to work with the resulting union: typed
constructors, a runtime schema-membership check, a curried is predicate
factory, bound matchers (match, map, fold and friends), and metadata.
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' }createUnion is the recommended starting point when you control the shape
of the union — it gives you a single source of truth for the schema, which
the rest of the API derives from automatically.
Constructors
Each schema entry becomes a typed factory function. Constructors return only
the data fields — createUnion injects the discriminant for you.
const r = Result.ok("hello"); // { type: 'ok', data: 'hello' }
const e = Result.error("fail"); // { type: 'error', message: 'fail' }
const l = Result.loading(); // { type: 'loading' }Constructors return plain objects with no runtime wrappers — they serialize over the wire, debug cleanly, and feed any other matcher unchanged. See Removing dismatch.
Re-exporting constructors
The factory is just a plain object, so you can destructure anything off it — constructors, matchers, predicates — and re-export them however you like. Two idioms cover the common cases.
Namespace style — keep the factory public, destructure for ergonomics:
// result.ts
export const Result = createUnion({
ok: (data: string) => ({ data }),
err: (message: string) => ({ message }),
});
export type Result = InferUnion<typeof Result>;
export const { ok, err } = Result;
// consumer.ts
import { Result, ok } from "./result";
const r = ok("hi");
const banner = Result.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:
// result.ts
const _Result = createUnion({
ok: (data: string) => ({ data }),
err: (message: string) => ({ message }),
});
export const { ok, err, match, matchWithDefault, is } = _Result;
export type Result = ReturnType<typeof ok> | ReturnType<typeof err>;
// consumer.ts
import { ok, match, type Result } from "./result";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.
isKnown
isKnown is the runtime schema-membership check — true if the value is
any declared variant, false otherwise. Use it at system boundaries.
Result.isKnown(apiResponse); // true if type is 'ok' | 'error' | 'loading'
Result.isKnown({ type: "unknown" }); // false
Result.isKnown(null); // falseThis is different from isUnion,
which only checks the structure (object with a string discriminant). isKnown
also confirms the variant tag is one of the declared ones.
is
The bound predicate factory — variant first, value second:
const errors = results.filter(Result.is("error"));
// ^? { type: 'error'; message: string }[]
const settled = results.filter(Result.is(["ok", "error"]));For value-first narrowing inside if blocks, use the standalone
is — both narrow correctly, but the
two forms exist for two different ergonomics.
Bound matchers
The full sync surface is exposed on the factory: match, matchWithDefault,
map, mapAll, fold, foldWithDefault, count, partition. They are
the same functions described in Standalone API,
pre-bound to the union type and discriminant — you don't need generics or
the discriminant argument.
const label = Result.match({
ok: ({ data }) => `Data: ${data}`,
error: ({ message }) => `Error: ${message}`,
loading: () => "Loading...",
});
const banner = Result.matchWithDefault({
error: ({ message }) => `Something went wrong: ${message}`,
Default: () => "All good",
});
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 }),
});Async helpers are standalone-only by design — see Async APIs.
InferUnion<typeof Factory>
Derive the inhabitants type from the factory:
type Result = InferUnion<typeof Result>;Use it whenever you need to type a variable, function parameter, or return type as the union itself.
Metadata
The factory exposes its variant list and discriminant as compile-time constants:
Result.variants; // readonly ['ok', 'error', 'loading']
Result.discriminant; // 'type'Useful for building runtime validators, logging, schema export, or reflection.
Custom discriminant
If your data already uses a different property name, pass it as the first argument:
const Event = createUnion("kind", {
click: (x: number) => ({ x }),
key: (code: string) => ({ code }),
});
type Event = InferUnion<typeof Event>;
// { kind: 'click'; x: number }
// | { kind: 'key'; code: string }The factory remains identical — only the injected discriminant key changes. Every standalone function also accepts a discriminant key as a trailing argument:
match(event, "kind")({ click: ..., key: ... });
is(event, "click", "kind");
isUnion(event, "kind");I recommend "type" when you control the shape of the union. It's the
default everywhere — no extra argument needed.
When to use what
createUnion— you control the shape, want one source of truth, and want constructors plus matchers plus runtime validation.- Standalone functions — the union already exists (third-party type,
reducer's
Action, JSON-parsed data) and you just want to dispatch. createPipeHandlers— the union exists, you want bound, reusable matchers that compose inpipe, but no constructors orisKnown.
Reach for what next
- For the no-factory style, see Standalone API.
- For binding handlers to an existing type, see
createPipeHandlers. - For an off-the-shelf async-state union, see RemoteData.