Building matchers
A matcher is assembled with pipe in three stages: you start it, add
ordered cases, then finish it with a finalizer. 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 }
// 1. start: Match.type<T>() produces a reusable (shape: Shape) => numberconst area = Match.type<Shape>().pipe( // 2. add cases: shape is narrowed to the Circle member inside this branch Match.tag("Circle", (shape) => Math.PI * shape.radius ** 2), Match.tag("Rect", (shape) => shape.width * shape.height), // 3. finish: 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"Flat tag dispatch
Section titled “Flat tag dispatch”When all you need is a switch-like table over a discriminated union’s _tag,
the *Tags constructors skip the pipe/finalizer ceremony entirely. They take
an object mapping each tag to a handler and dispatch directly. There are three
flavours depending on whether you want an immediate result, a reusable
function, or a pipeline finalizer.
Match.valueTags matches one value immediately and returns the handler’s
result directly — no finalizer, no function to call later.
import { Match } from "effect"
type Status = | { readonly _tag: "Success"; readonly data: string } | { readonly _tag: "Failure"; readonly error: string }
const success: Status = { _tag: "Success", data: "Hello" }
const message = Match.valueTags(success, { Success: (s) => `ok: ${s.data}`, Failure: (f) => `err: ${f.error}`})
console.log(message) // => "ok: Hello"Match.typeTags<I>() builds a reusable function from the same handler map.
It has two overloads: pass a return type (typeTags<I, Ret>()) to force every
branch to return Ret, or omit it (typeTags<I>()) to infer the union of all
branch results.
import { Match } from "effect"
type Result = | { readonly _tag: "Success"; readonly data: string } | { readonly _tag: "Error"; readonly message: string } | { readonly _tag: "Loading" }
// Pinned return type: every branch must return a stringconst formatResult = Match.typeTags<Result, string>()({ Success: (r) => `Data: ${r.data}`, Error: (r) => `Error: ${r.message}`, Loading: () => "Loading..."})
console.log(formatResult({ _tag: "Success", data: "Hi" })) // => "Data: Hi"
// Inferred return type: the union of all branch resultsconst processResult = Match.typeTags<Result>()({ Success: (r) => ({ type: "ok", value: r.data }), Error: (r) => ({ type: "error", error: r.message }), Loading: () => ({ type: "pending" })})
console.log(processResult({ _tag: "Loading" })) // => { type: "pending" }The three tag-dispatch entry points are easy to confuse:
| API | Shape | Result |
|---|---|---|
Match.valueTags(input, handlers) | takes the value, immediate | the handler’s return value |
Match.typeTags<I>()(handlers) | reusable | (input: I) => Ret |
Match.tagsExhaustive(handlers) | a pipeline step (after Match.type/value) | finalizes the matcher (see below) |
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"Match.whenOr
Section titled “Match.whenOr”Runs the handler if any of the supplied patterns match. The handler’s input is narrowed to the union of the patterns.
import { Match } from "effect"
type ErrorType = | { readonly _tag: "NetworkError" } | { readonly _tag: "TimeoutError" } | { readonly _tag: "ValidationError"; readonly field: string }
const handle = Match.type<ErrorType>().pipe( Match.whenOr( { _tag: "NetworkError" }, { _tag: "TimeoutError" }, () => "Retry the request" ), Match.when({ _tag: "ValidationError" }, (e) => `Invalid: ${e.field}`), Match.exhaustive)
console.log(handle({ _tag: "NetworkError" })) // => "Retry the request"console.log(handle({ _tag: "ValidationError", field: "email" })) // => "Invalid: email"Match.whenAnd
Section titled “Match.whenAnd”Runs the handler only if all supplied patterns match. The patterns are intersected, so the handler sees a value satisfying every one.
import { Match } from "effect"
type User = { readonly age: number; readonly role: "admin" | "user" }
const check = Match.type<User>().pipe( Match.whenAnd( { age: (n: number) => n >= 18 }, { role: "admin" }, () => "Admin access granted" ), Match.orElse(() => "Access denied"))
console.log(check({ age: 20, role: "admin" })) // => "Admin access granted"console.log(check({ age: 20, role: "user" })) // => "Access denied"Match.not
Section titled “Match.not”The inverse of when: it matches every input except those covered by the
pattern. Excluded values bypass the handler and continue through later cases.
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 refinements
Section titled “Built-in refinements”Patterns can use these refinements to match by type instead of by value. They
compose inside object patterns too (e.g. { a: Match.number }). Each is shown
with a short example below.
| Refinement | Matches |
|---|---|
Match.string | values of type string |
Match.nonEmptyString | non-empty strings |
Match.number | values of type number (incl. NaN, infinities) |
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 | non-null, non-array objects |
Match.null | the literal null |
Match.undefined | the literal 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 class, with type-safe narrowing |
Match.instanceOfUnsafe(Class) | instances of a class, without the narrowing guarantee |
Match.string
Section titled “Match.string”Refines unknown to string.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.string, (s) => s.toUpperCase()), Match.orElse(() => "not a string"))
console.log(f("hi")) // => "HI"console.log(f(42)) // => "not a string"Match.nonEmptyString
Section titled “Match.nonEmptyString”Matches strings of length greater than zero (whitespace-only strings still count as non-empty).
import { Match } from "effect"
const f = Match.type<string>().pipe( Match.when(Match.nonEmptyString, (s) => `valid: ${s}`), Match.orElse(() => "empty"))
console.log(f("hello")) // => "valid: hello"console.log(f("")) // => "empty"Match.number
Section titled “Match.number”Refines unknown to number, including NaN, Infinity, and -Infinity.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.number, (n) => `number: ${n}`), Match.orElse(() => "not a number"))
console.log(f(3.14)) // => "number: 3.14"console.log(f("3.14")) // => "not a number"Match.boolean
Section titled “Match.boolean”Refines unknown to the primitive boolean values true / false.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.boolean, (b) => (b ? "yes" : "no")), Match.orElse(() => "not a boolean"))
console.log(f(true)) // => "yes"console.log(f(0)) // => "not a boolean"Match.bigint
Section titled “Match.bigint”Refines unknown to bigint.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.bigint, (b) => `bigint: ${b}`), Match.orElse(() => "not a bigint"))
console.log(f(123n)) // => "bigint: 123"console.log(f(123)) // => "not a bigint"Match.symbol
Section titled “Match.symbol”Refines unknown to symbol.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.symbol, (s) => `symbol: ${String(s)}`), Match.orElse(() => "not a symbol"))
console.log(f(Symbol("x"))) // => "symbol: Symbol(x)"console.log(f("x")) // => "not a symbol"Match.date
Section titled “Match.date”Matches Date instances only — not date strings or numeric timestamps.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.date, (d) => d.getFullYear()), Match.orElse(() => "not a Date"))
console.log(f(new Date("2024-01-01"))) // => 2024console.log(f("2024-01-01")) // => "not a Date"Match.record
Section titled “Match.record”Backed by Predicate.isObject: matches non-null values whose runtime type is
"object" and that are not arrays. Note that Date, RegExp, and class
instances also pass — use instanceOf to distinguish them.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.record, (obj) => Object.keys(obj).length), Match.orElse(() => "not a record"))
console.log(f({ a: 1, b: 2 })) // => 2console.log(f([1, 2, 3])) // => "not a record"Match.defined
Section titled “Match.defined”Matches any value that is neither null nor undefined.
import { Match } from "effect"
const f = Match.type<string | null | undefined>().pipe( Match.when(Match.defined, (v) => `defined: ${v}`), Match.orElse(() => "nullish"))
console.log(f("hi")) // => "defined: hi"console.log(f(null)) // => "nullish"Match.null
Section titled “Match.null”Matches only the literal null. Backed by Predicate.isNull.
import { Match } from "effect"
const f = Match.type<string | null | undefined>().pipe( Match.when(Match.null, () => "was null"), Match.orElse((v) => `other: ${v}`))
console.log(f(null)) // => "was null"console.log(f("hi")) // => "other: hi"Match.undefined
Section titled “Match.undefined”Matches only the literal undefined. Backed by Predicate.isUndefined.
import { Match } from "effect"
const f = Match.type<string | null | undefined>().pipe( Match.when(Match.undefined, () => "was undefined"), Match.orElse((v) => `other: ${v}`))
console.log(f(undefined)) // => "was undefined"console.log(f("hi")) // => "other: hi"Match.any
Section titled “Match.any”Matches every input with no narrowing — including null, undefined, objects,
and functions. Put it last, since the first matching case wins.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.string, (s) => `string: ${s}`), Match.when(Match.any, (v) => `other: ${typeof v}`), Match.exhaustive)
console.log(f("hi")) // => "string: hi"console.log(f(null)) // => "other: object"Match.is
Section titled “Match.is”Matches one of a set of literal primitive (or null) values.
import { Match } from "effect"
const f = Match.type<string | number>().pipe( Match.when(Match.is("ok", 200), () => "success"), Match.when(Match.is("error", 500), () => "failure"), Match.orElse((v) => `unknown: ${v}`))
console.log(f(200)) // => "success"console.log(f("error")) // => "failure"console.log(f("pending")) // => "unknown: pending"Match.instanceOf
Section titled “Match.instanceOf”Matches instances of a constructor and narrows the handler argument to the instance type.
import { Match } from "effect"
const f = Match.type<unknown>().pipe( Match.when(Match.instanceOf(Map), (m) => `map size: ${m.size}`), Match.when(Match.instanceOf(Error), (e) => `error: ${e.message}`), Match.orElse(() => "other"))
console.log(f(new Map([["a", 1]]))) // => "map size: 1"console.log(f(new Error("boom"))) // => "error: boom"Match.instanceOfUnsafe
Section titled “Match.instanceOfUnsafe”Same runtime instanceof check as Match.instanceOf, but it does not
remove the matched type from the remaining/unmatched set. Use it when you need
the looser refinement and are willing to narrow the handler argument manually;
prefer Match.instanceOf for normal type-safe matching.
import { Match } from "effect"
class CustomError extends Error { constructor(message: string, readonly code: number) { super(message) }}
const f = Match.type<unknown>().pipe( Match.when(Match.instanceOfUnsafe(CustomError), (err) => { // narrowing is up to you here const e = err as CustomError return `code ${e.code}: ${e.message}` }), Match.orElse(() => "not a CustomError"))
console.log(f(new CustomError("nope", 404))) // => "code 404: nope"console.log(f("x")) // => "not a CustomError"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. The tag family matches against it
directly and narrows the handler to that member.
Match.tag
Section titled “Match.tag”Matches one or more _tag values with a single 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", () => "…"), Match.tag("Success", (e) => `data: ${e.data}`), Match.tag("Failure", (e) => `error: ${e.error.message}`), Match.exhaustive)
console.log(render({ _tag: "Success", data: "hi" })) // => "data: hi"console.log(render({ _tag: "Loading" })) // => "…"Match.tagStartsWith
Section titled “Match.tagStartsWith”Matches any member whose _tag begins with a given prefix — handy for
hierarchical or namespaced tags like "A.A".
import { Match } from "effect"
const f = Match.type<{ _tag: "A" } | { _tag: "B" } | { _tag: "A.A" }>().pipe( Match.tagStartsWith("A", () => 1 as const), Match.tagStartsWith("B", () => 2 as const), Match.orElse(() => 3 as const))
console.log(f({ _tag: "A" })) // => 1console.log(f({ _tag: "A.A" })) // => 1console.log(f({ _tag: "B" })) // => 2Match.tags
Section titled “Match.tags”Takes an object mapping each _tag to a handler. It does not finalize the
matcher — handlers may be partial, and the pipeline continues, so you still need
a finalizer afterwards.
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( // Handle only some tags here… Match.tags({ Success: (e) => `data: ${e.data}`, Failure: (e) => `error: ${e.error.message}` }), // …then close the remaining ones Match.orElse(() => "…"))
console.log(render({ _tag: "Loading" })) // => "…"Match.tagsExhaustive
Section titled “Match.tagsExhaustive”The finalizing variant of Match.tags: every _tag must have a handler, so it
closes the matcher directly — no Match.exhaustive needed.
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( // Omit a tag and this stops compiling Match.tagsExhaustive({ Loading: () => "…", Success: (e) => `data: ${e.data}`, Failure: (e) => `error: ${e.error.message}` }))
console.log(render({ _tag: "Failure", error: new Error("boom") }))// => "error: boom"Matching other discriminators
Section titled “Matching other discriminators”The discriminator family mirrors the tag family exactly, but for any field
you name instead of the hard-wired _tag. Use it for unions discriminated by
type, kind, status, and so on.
Match.discriminator
Section titled “Match.discriminator”Matches one or more exact values of a named discriminator field.
import { Match } from "effect"
type Action = | { readonly type: "increment"; readonly by: number } | { readonly type: "decrement"; readonly by: number } | { readonly type: "reset" }
const reduce = Match.type<Action>().pipe( Match.discriminator("type")("increment", "decrement", (a) => a.by), Match.discriminator("type")("reset", () => 0), Match.exhaustive)
console.log(reduce({ type: "increment", by: 5 })) // => 5console.log(reduce({ type: "reset" })) // => 0Match.discriminatorStartsWith
Section titled “Match.discriminatorStartsWith”Matches members whose discriminator field starts with a prefix — the
tagStartsWith equivalent for arbitrary fields.
import { Match } from "effect"
const f = Match.type<{ type: "A" } | { type: "B" } | { type: "A.A" }>().pipe( Match.discriminatorStartsWith("type")("A", () => 1 as const), Match.discriminatorStartsWith("type")("B", () => 2 as const), Match.orElse(() => 3 as const))
console.log(f({ type: "A" })) // => 1console.log(f({ type: "A.A" })) // => 1console.log(f({ type: "B" })) // => 2Match.discriminators
Section titled “Match.discriminators”An object map of discriminator value to handler, the non-finalizing counterpart
of Match.discriminator (and the arbitrary-field version of Match.tags).
import { Match } from "effect"
type Shape = | { readonly kind: "circle"; readonly r: number } | { readonly kind: "square"; readonly side: number }
const area = Match.type<Shape>().pipe( Match.discriminators("kind")({ circle: (c) => Math.PI * c.r ** 2, square: (s) => s.side ** 2 }), Match.orElse(() => 0))
console.log(area({ kind: "square", side: 3 })) // => 9Match.discriminatorsExhaustive
Section titled “Match.discriminatorsExhaustive”The finalizing object-map variant: every discriminator value must be handled, so
the matcher is closed directly (the arbitrary-field version of
Match.tagsExhaustive).
import { Match } from "effect"
type Shape = | { readonly kind: "circle"; readonly r: number } | { readonly kind: "square"; readonly side: number }
const area = Match.type<Shape>().pipe( Match.discriminatorsExhaustive("kind")({ circle: (c) => Math.PI * c.r ** 2, square: (s) => s.side ** 2 }))
console.log(area({ kind: "circle", r: 1 })) // => 3.14159...Finishing a matcher
Section titled “Finishing a matcher”Every matcher pipeline must be closed with a finalizer. The choice encodes what
should happen when no case matches. (The *Exhaustive and *Tags helpers above
finalize on their own.)
Match.exhaustive
Section titled “Match.exhaustive”Compiles only when every possible input is handled, then produces the matcher
function (or, for Match.value, the result). Throws at runtime if narrowing was
somehow bypassed. Use it for closed unions where a missed case is a bug.
import { Match } from "effect"
const f = Match.type<"a" | "b">().pipe( Match.when("a", () => 1), Match.when("b", () => 2), Match.exhaustive)
console.log(f("a")) // => 1Match.orElse
Section titled “Match.orElse”Runs a fallback handler for any unmatched input, so the match need not be exhaustive. The handler receives the remaining (unmatched) input.
import { Match } from "effect"
const f = Match.type<string | number>().pipe( Match.when("a", () => "ok"), Match.orElse((v) => `fallback: ${v}`))
console.log(f("a")) // => "ok"console.log(f("b")) // => "fallback: b"Match.orElseAbsurd
Section titled “Match.orElseAbsurd”Finalizes a matcher that you believe handles every case, without requiring a
fallback handler or compile-time exhaustiveness proof: if an unmatched value
reaches it at runtime, it throws. Unlike Match.exhaustive, it does not require
the type system to have proven the union empty — so reach for exhaustive when
you want the compiler to catch the missing case, and orElseAbsurd only when an
unmatched input is genuinely impossible.
import { Match } from "effect"
const f = Match.type<"a" | "b">().pipe( Match.when("a", () => "Found A"), Match.when("b", () => "Found B"), Match.orElseAbsurd)
console.log(f("a")) // => "Found A"// f("c" as any) // throws at runtimeMatch.option
Section titled “Match.option”Returns the result as an Option: Option.some(value)
on a match, Option.none() otherwise.
import { Match } from "effect"
type User = { readonly role: "admin" | "editor" | "viewer" }
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" }))// => { _id: 'Option', _tag: 'Some', value: 'full access' }console.log(access({ role: "viewer" }))// => { _id: 'Option', _tag: 'None' }Match.result
Section titled “Match.result”Returns a Result: Result.succeed(value) on a match,
or a failure carrying the unmatched input. This is the v4 replacement for the v3
Match.either finalizer.
import { Match } from "effect"
type User = { readonly role: "admin" | "editor" | "viewer" }
const getRole = Match.type<User>().pipe( Match.when({ role: "admin" }, () => "full access"), Match.when({ role: "editor" }, () => "can edit"), Match.result)
console.log(getRole({ role: "admin" }))// => { _id: 'Result', _tag: 'Success', value: 'full access' }console.log(getRole({ role: "viewer" }))// => { _id: 'Result', _tag: 'Failure', failure: { role: 'viewer' } }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)Model types
Section titled “Model types”A few exported types describe the matcher itself. You rarely name them by hand, but they appear in inferred signatures and error messages:
Matcher— the union of the two concrete matcher kinds.TypeMatcher— produced byMatch.type<T>(); finalizes to a reusable function.ValueMatcher— produced byMatch.value(x); carries the input and finalizes to a result.Case— a single accumulated case, defined asWhen | Not(the positive and negative case shapes).SafeRefinementis the type behind the built-in refinements likeMatch.stringandMatch.is.