Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Going further
  2. Pattern matching

Pattern matching#

craftMatch is type-safe pattern matching over a bare literal union (a string / number / enum union). It is the value-level counterpart of the exception-level catchTag / catchTag.exhaustive pair: one call for a single case, a .exhaustive variant whose handler map is checked to cover the union at compile time.

Use it when a literal union drives a decision and forgetting a case should be a build error — a status, a role, a mode. Not for a two-way boolean; a ternary is clearer.

Use it wherever a switch would go — mapping a status to a label, an icon, a component input — but with a compiler that refuses to build when you add a member to the union and forget a branch.

Why not a plain switch?#

A switch (or an object lookup) over a union has three recurring problems:

ts
type Status = 'active' | 'idle' | 'error';

function label(status: Status) {
  switch (status) {
    case 'active':
      return 'Running';
    case 'idle':
      return 'Waiting';
    // 'error' forgotten → no error, `label` silently returns undefined
  }
}
Concernswitch / object lookupcraftMatch.exhaustive
Missing a union membersilent undefined at runtimecompile error
Handler for a non-membersilent dead codecompile error
Value passed to each branchwidened to the whole unionnarrowed to its own literal
Return typeunion incl. undefined unless you assertexact union of the branch returns

Signature#

ts
// Single case — optional match
craftMatch<Value extends string | number, Case extends Value, R>(
  value: Value,
  matchCase: Case,
  handler: (value: Case) => R,
): R | undefined;

// Exhaustive — every union member needs a handler
craftMatch.exhaustive<Value extends string | number, R>(
  value: Value,
  handlers: { [K in Value]: (value: K) => R },
): R;

Exhaustiveness is enforced natively by the mapped handler type { [K in Value]: (value: K) => R } — the union members are the required keys, so a missing key or a key outside the union is a plain type error, and each handler receives its own narrowed literal.

Single case#

Runs the handler only when value equals matchCase, otherwise returns undefined:

ts
import { craftMatch } from '@craft-ts/core';

const status = 'active' as Status;

const spinner = craftMatch(status, 'active', () => '⏳'); // '⏳'
const nope = craftMatch(status, 'error', () => '💥'); // undefined

The handler's argument is narrowed to the matched literal ('active' above), and the result is typed R | undefined.

Exhaustive match#

The handler map must cover exactly the union — no more, no less:

ts
import { craftMatch } from '@craft-ts/core';

const label = (status: Status) =>
  craftMatch.exhaustive(status, {
    active: () => 'Running',
    idle: () => 'Waiting',
    error: () => 'Failed',
  });

Add 'pending' to Status and every craftMatch.exhaustive over it stops compiling until you add the branch — the exhaustiveness wall you would otherwise hand-roll with a never assertion in a switch's default.

Each handler is narrowed to its own literal, and the result is the union of the branch return types:

ts
const view = craftMatch.exhaustive(status, {
  active: () => ({ color: 'green', text: 'Running' }),
  idle: () => ({ color: 'gray', text: 'Waiting' }),
  error: () => ({ color: 'red', text: 'Failed' }),
});
// view: { color: string; text: string }

The compile errors it catches#

ts
// ❌ missing a member — the union is not fully covered
craftMatch.exhaustive(status, {
  active: () => 'Running',
  idle: () => 'Waiting',
}); // Type error: property 'error' is missing

// ❌ a handler for something outside the union
craftMatch.exhaustive(status, {
  active: () => 'Running',
  idle: () => 'Waiting',
  error: () => 'Failed',
  unknown: () => 'nope', // Type error: 'unknown' is not in Status
});

With enums#

A TypeScript enum compiles to a string | number union, so it works unchanged:

ts
enum Tab {
  Overview = 'overview',
  Billing = 'billing',
  Team = 'team',
}

const title = (tab: Tab) =>
  craftMatch.exhaustive(tab, {
    [Tab.Overview]: () => 'Overview',
    [Tab.Billing]: () => 'Billing',
    [Tab.Team]: () => 'Team',
  });

Scope & limits#

  • Bare literal unions only. craftMatch matches on the value itself, not on a discriminant field — it does not (yet) take a union of objects keyed by a type / kind field. Map the discriminant to a literal first if you need that: craftMatch.exhaustive(shape.kind, { … }).

  • Pure and synchronous. Unlike catchTag, craftMatch is not a craft program: it does not yield* and does not track dependencies. To run a different craft program per branch, yield* inside the handler bodies of a normal generator instead.

  • No catch-all. There is the exhaustive form (compile wall) or the single-case form (optional undefined) — there is no .otherwise(fallback).

API#

ExportPurpose
craftMatch(value, case, handler)Match a single literal; returns R | undefined.
craftMatch.exhaustive(value, handlers)Match every member; compile-time exhaustive; returns the union of branch results.
CraftMatchHandlers<Value, R>The { [K in Value]: (value: K) => R } handler-map type.

See Also#

  • Program operators — the exception-level counterpart

  • Exceptions as values

←Program operatorsObservability→

On this page

  • Why not a plain switch?
  • Signature
  • Single case
  • Exhaustive match
  • The compile errors it catches
  • With enums
  • Scope & limits
  • API
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search