Building matchers
A matcher is assembled with pipe: you start it, add ordered cases, then finish
it. Cases are evaluated top to bottom and the first match wins, so order
matters — put specific cases before catch-alls. The type system narrows the
handler argument for each case and tracks the inputs that are still unmatched,
which is what makes the Match.exhaustive finalizer safe.
import { Match } from "effect"
type Shape = | { readonly _tag: "Circle"; readonly radius: number } | { readonly _tag: "Rect"; readonly width: number; readonly height: number }
// Match.type<T>() produces a reusable (shape: Shape) => number functionconst area = Match.type<Shape>().pipe( // event is narrowed to the Circle member inside this branch Match.tag("Circle", (event) => Math.PI * event.radius ** 2), Match.tag("Rect", (event) => event.width * event.height), // Drop a tag above and this line stops compiling Match.exhaustive)
console.log(area({ _tag: "Circle", radius: 2 })) // 12.566...console.log(area({ _tag: "Rect", width: 3, height: 4 })) // 12Match by type
Section titled “Match by type”Match.type<T>() builds a reusable matcher: the result is a function you can
call with many different inputs. Use it when the same branch table is applied
in more than one place, or when you want exhaustiveness checking against a
union type.
import { Match } from "effect"
// ┌─── (u: string | number) => string// ▼const format = Match.type<string | number>().pipe( // Match.number / Match.string are built-in type refinements Match.when(Match.number, (n) => `number: ${n}`), Match.when(Match.string, (s) => `string: ${s}`), Match.exhaustive)
console.log(format(0)) // "number: 0"console.log(format("hello")) // "string: hello"Match by value
Section titled “Match by value”Match.value(x) classifies a single value right away. The matcher already
contains the input, so the finalizer returns the result directly instead of a
function. Use it for one-off branching where you don’t need a reusable matcher.
import { Match } from "effect"
const input = { name: "John", age: 30 }
const result = Match.value(input).pipe( // Object patterns match by comparing the listed fields Match.when({ name: "John" }, (user) => `${user.name} is ${user.age}`), // orElse supplies a fallback, so the match need not be exhaustive Match.orElse(() => "not John"))
console.log(result) // "John is 30"Defining cases with when
Section titled “Defining cases with when”Match.when is the general-purpose case. Its first argument is a pattern,
which can be a literal value, a predicate function, or an object whose fields
are themselves patterns. The handler runs with the input narrowed to whatever
the pattern accepts.
import { Match } from "effect"
const classify = Match.type<{ age: number }>().pipe( // Field pattern with a predicate: matches when age > 18 Match.when({ age: (age: number) => age > 18 }, (user) => `adult: ${user.age}`), // Field pattern with a literal: matches when age is exactly 18 Match.when({ age: 18 }, () => "just eligible"), Match.orElse((user) => `minor: ${user.age}`))
console.log(classify({ age: 20 })) // "adult: 20"console.log(classify({ age: 18 })) // "just eligible"console.log(classify({ age: 4 })) // "minor: 4"Two combinators extend when for multi-pattern cases:
Match.whenOr(patternA, patternB, handler)runs the handler if any of the patterns match.Match.whenAnd(patternA, patternB, handler)runs the handler only if all patterns match.
Match.not(pattern, handler) is the inverse of when: it matches every input
except those covered by the pattern.
import { Match } from "effect"
const greet = Match.type<string | number>().pipe( // Matches anything that is not the literal "hi" Match.not("hi", () => "ok"), Match.orElse(() => "fallback"))
console.log(greet("hello")) // "ok"console.log(greet("hi")) // "fallback"Built-in predicates
Section titled “Built-in predicates”Patterns can use these refinements to match by type instead of by value. They
compose inside object patterns too (e.g. { a: Match.number }).
| Predicate | Matches |
| ------------------------- | --------------------------------------------------------------- |
| Match.string | values of type string |
| Match.nonEmptyString | non-empty strings |
| Match.number | values of type number |
| Match.boolean | values of type boolean |
| Match.bigint | values of type bigint |
| Match.symbol | values of type symbol |
| Match.date | instances of Date |
| Match.record | objects keyed by string/symbol with unknown values |
| Match.null | the value null |
| Match.undefined | the value undefined |
| Match.defined | any non-null, non-undefined value |
| Match.any | any value, with no narrowing |
| Match.is(...values) | one of the given literal values, e.g. Match.is("a", 42, true) |
| Match.instanceOf(Class) | instances of a given class |
import { Match } from "effect"
// PropertyKey is string | number | symbolconst describeKey = Match.type<PropertyKey>().pipe( Match.when(Match.number, (n) => `number key: ${n}`), Match.when(Match.string, (s) => `string key: ${s}`), Match.when(Match.symbol, (s) => `symbol key: ${String(s)}`), Match.exhaustive)
console.log(describeKey(42)) // "number key: 42"console.log(describeKey("name")) // "string key: name"Matching tagged unions
Section titled “Matching tagged unions”Effect’s data types (errors, Option, Result, schema variants) all use a
_tag field as their discriminator. Match.tag matches against it directly and
narrows the handler to that member. You can pass several tags to one handler.
import { Match } from "effect"
type RemoteData = | { readonly _tag: "Loading" } | { readonly _tag: "Success"; readonly data: string } | { readonly _tag: "Failure"; readonly error: Error } | { readonly _tag: "Cancelled" }
const render = Match.type<RemoteData>().pipe( // One handler can cover multiple tags Match.tag("Loading", "Cancelled", () => "…"), // event is narrowed to the Success member here Match.tag("Success", (event) => `data: ${event.data}`), Match.tag("Failure", (event) => `error: ${event.error.message}`), Match.exhaustive)
console.log(render({ _tag: "Success", data: "hi" })) // "data: hi"console.log(render({ _tag: "Loading" })) // "…"For a flat, switch-like table over tags, Match.tags (and the
self-finalizing Match.tagsExhaustive) take an object mapping each tag to its
handler:
import { Match } from "effect"
type RemoteData = | { readonly _tag: "Loading" } | { readonly _tag: "Success"; readonly data: string } | { readonly _tag: "Failure"; readonly error: Error }
const render = Match.type<RemoteData>().pipe( // tagsExhaustive finalizes the matcher; every tag must appear Match.tagsExhaustive({ Loading: () => "…", Success: (event) => `data: ${event.data}`, Failure: (event) => `error: ${event.error.message}` }))
console.log(render({ _tag: "Failure", error: new Error("boom") }))// Output: "error: boom"import { Match } from "effect"
// A union discriminated by "type" rather than "_tag"type Action = | { readonly type: "increment"; readonly by: number } | { readonly type: "reset" }
const reduce = Match.type<Action>().pipe( Match.discriminator("type")("increment", (event) => event.by), Match.discriminator("type")("reset", () => 0), Match.exhaustive)
console.log(reduce({ type: "increment", by: 5 })) // 5Finishing a matcher
Section titled “Finishing a matcher”Every matcher must be closed with a finalizer. The choice encodes what should happen when no case matches.
Match.exhaustive— compiles only when every possible input is handled; throws at runtime if narrowing was somehow bypassed. Use it for closed unions where a missed case is a bug.Match.orElse(handler)— runs a fallback handler for any unmatched input, so the match need not be exhaustive.Match.option— returns the result as anOption:Option.some(value)on a match,Option.none()otherwise.Match.result— returns aResult:Result.succeed(value)on a match, orResult.fail(input)carrying the unmatched input. This is the v4 replacement for the v3Match.eitherfinalizer.
import { Match } from "effect"
type User = { readonly role: "admin" | "editor" | "viewer" }
// Not every role is handled — option turns a miss into Option.none()const access = Match.type<User>().pipe( Match.when({ role: "admin" }, () => "full access"), Match.when({ role: "editor" }, () => "can edit"), Match.option)
console.log(access({ role: "admin" }))// Output: { _id: 'Option', _tag: 'Some', value: 'full access' }console.log(access({ role: "viewer" }))// Output: { _id: 'Option', _tag: 'None' }Pinning the return type
Section titled “Pinning the return type”By default the result type is the union of every branch’s return type. Call
Match.withReturnType<T>() first to force every branch to return T, so a
branch that returns the wrong type is rejected where it is defined.
import { Match } from "effect"
const match = Match.type<{ a: number } | { b: string }>().pipe( // Must come first in the pipeline Match.withReturnType<string>(), // @ts-expect-error — a is a number, not a string Match.when({ a: Match.number }, (_) => _.a), Match.when({ b: Match.string }, (_) => _.b), Match.exhaustive)