Pattern matching with match and is
match picks the first arm whose pattern fits a value. is tests a value against one pattern. Both come from the TC39 pattern matching proposal (stage 1). They compile to plain conditionals, so TypeScript narrows the value in each arm, and an arm can call Effect functions.
match
// run: Console.log(JSON.stringify([describe(new NotFound({ id: "7" })), describe(new Timeout()), describe(new Denied({ user: "Ada", reason: "admin" })), describe(new Denied({ user: "Ada", reason: "other" }))]))
error NotFound { id: string }
error Timeout
error Denied { user: string; reason: string }
type Failure = NotFound | Timeout | Denied
function describe(failure: Failure) {
return match (failure) {
when NotFound and { let id }: `no user ${id}`;
when Timeout: "retry";
when Denied and { reason: "root" or "admin" }: "denied by policy";
when Denied: `denied ${failure.user}`;
}
}
// Output: ["no user 7","retry","denied by policy","denied Ada"]Types in this example
error- class NotFound
NotFound- class NotFound
id- (property) id: string
error- class Timeout
Timeout- class Timeout
error- class Denied
Denied- class Denied
user- (property) user: string
reason- (property) reason: string
Failure- type Failure = Denied | NotFound | Timeout
function- function describe(failure: Failure): string!!
describe- function describe(failure: Failure): stringPlain JavaScript: VibeLang code calls it directly. TypeScript callers see
Effect<string, never, never>. failure- (parameter) failure: Failure
id- let id: string
failure- (parameter) failure: Denied
Each arm is when pattern: expression;. Arms are tried in order, and the first match gives the value. A default: expression; arm matches anything and must come last. match is an expression, so it can appear after return, in a const initializer, or as an argument.
An arm is one expression. Use do { } for several statements; its value is its last statement's:
// run: Console.log(retryDelay(new Timeout())); Console.log(retryDelay(new NotFound({ id: "7" })))
function retryDelay(failure: Failure) {
return match (failure) {
when Timeout: do {
Console.log("timed out")
"1 second"
};
default: "0 millis";
}
}
// Output: timed out
// Output: 1 second
// Output: 0 millisTypes in this example
retryDelay- function retryDelay(failure: Failure): string!!
failure- (parameter) failure: Failure
Failure- (alias) type Failure = Denied | NotFound | Timeout
Timeout- (alias) class Timeout
Console- (alias) module "effect/Console"
log- const log: (...args: ReadonlyArray<any>) => void!!Runs here and returns
void.Logs a general-purpose message to the console.
A function named match still works. Only match (value) { followed by when, default, or } starts a match.
Patterns
| Pattern | Matches | Example |
|---|---|---|
| Literal | An equal string, number, boolean, null, or undefined | when 404: |
| Binding | Anything, and names it with let, const, or var | when let other: |
| Array | An array of that length; ... allows more | when [let first, ...let rest]: |
| Object | A value with those properties; { let id } binds id | when { status: 200, let body }: |
| Matcher | An instance of a class, or what a custom matcher accepts | when NotFound: |
| Extractor | A matcher's values, matched by the list in parentheses | when Option.Some(let user): |
and, or, not | Conjunction, alternative, or negation | when "a" or "e": |
| Guard | A condition that is true | when [let n] and if (n > 0): |
// run: Console.log(JSON.stringify([shape([]), shape([2]), shape([2, 3, 4]), shape({ kind: "box", size: 5 }), shape({ kind: "point", size: 1 }), shape({ kind: "other" })]))
function shape(value: ReadonlyArray<number> | { kind: string; size?: number }) {
return match (value) {
when []: "empty";
when [let only]: `one: ${only}`;
when [let first, ...let rest]: `${first} then ${rest.length} more`;
when { kind: "box", let size }: `box of ${size}`;
when { kind: "point", ...let others }: `point with ${Object.keys(others).length} more keys`;
default: "other";
}
}
// Output: ["empty","one: 2","2 then 2 more","box of 5","point with 1 more keys","other"]Types in this example
function- function shape(value: ReadonlyArray<number> | { readonly kind: string; readonly size?: number; }): string!!
shape- function shape(value: ReadonlyArray<number> | { readonly kind: string; readonly size?: number; }): stringPlain JavaScript: VibeLang code calls it directly. TypeScript callers see
Effect<string, never, never>. value- (parameter) value: readonly number[] | { readonly kind: string; readonly size?: number; }
ReadonlyArray- interface ReadonlyArray<T>
kind- (property) kind: string
size- (property) size?: number | undefined
only- let only: any
first- let first: any
rest- let rest: any[]
length- (property) Array<any>.length: numberGets or sets the length of the array. This is a number one higher than the highest index in the array.
size- let size: number | undefined
others- let others: Omit<{ readonly kind: string; readonly size?: number; }, "kind">
Object- var Object: ObjectConstructorProvides functionality common to all JavaScript objects.
keys- (method) ObjectConstructor.keys(o: {}): string[]Returns the names of the enumerable string properties and methods of an object.
length- (property) Array<string>.length: numberGets or sets the length of the array. This is a number one higher than the highest index in the array.
An object pattern checks only the properties it names, and ...let rest collects the others. A property that is not a valid name needs quotes, { "content-type": let type }, and a computed key must be a name or a literal, { [key]: let value }.
Option, Exit, Result, and primitives
These matchers compile to the module's own test, so the bindings have exact types:
| Matcher | Test | Extracts |
|---|---|---|
Option.Some, Option.None | Option.isSome, Option.isNone | the value |
Exit.Success, Exit.Failure | Exit.isSuccess, Exit.isFailure | the value, the cause |
Result.Success, Result.Failure | Result.isSuccess, Result.isFailure | the success, the failure |
String, Number, Boolean, BigInt, Symbol | typeof | the value |
// run: Console.log(greeting(Option.some("Ada"))); Console.log(greeting(Option.none())); Console.log(report("ada")); Console.log(report("").includes("NotFound"))
function greeting(name: Option.Option<string>) {
return match (name) {
when Option.Some(let n): `Hello, ${n}`;
when Option.None: "Hello";
}
}
function report(id: string) {
const outcome = exit(getUser(id))
return match (outcome) {
when Exit.Success(let user): user.name;
when Exit.Failure(let cause): Cause.pretty(cause);
}
}
function getUser(id: string): { name: string }!NotFound {
if (id === "") throw new NotFound({ id })
return { name: "Ada" }
}
// Output: Hello, Ada
// Output: Hello
// Output: Ada
// Output: trueTypes in this example
function- function greeting(name: Option.Option<string>): string!!
greeting- function greeting(name: Option.Option<string>): stringPlain JavaScript: VibeLang code calls it directly. TypeScript callers see
Effect<string, never, never>. name- (parameter) name: Option<string>
Option- (alias) module "effect/Option"
Option- type Option<A> = None<A> | Some<A>The
Optiondata type represents optional values. AnOption<A>is eitherSome<A>, containing a value of typeA, orNone, representing absence. n- let n: string
report- function report(id: string): string!!
id- (parameter) id: string
outcome- const outcome: Exit<{ readonly name: string; }, NotFound>
exit- const exit: <{
readonly name: string;
}, NotFound, never>(self: {
readonly name: string;
}!NotFound!) => Exit<{
readonly name: string;
}, NotFound>!!Runs here and returns
Exit<{ readonly name: string; }, NotFound>.Transforms an effect to encapsulate both failure and success using theExitdata type. getUser- function getUser(id: string): {
readonly name: string;
}!NotFound!Passed by name to
exitas a program: it runs when that program runs, not here, and returns{ readonly name: string; }. user- let user: { readonly name: string; }
name- (property) name: string
cause- let cause: Cause<NotFound>
Cause- (alias) module "effect/Cause"
pretty- const pretty: <NotFound>(cause: Cause<NotFound>) => stringFormats a
Causeas a human-readable string for logging or debugging. getUser- function getUser(id: string): { readonly name: string; }!NotFound!
NotFound- (alias) class NotFound
NotFound- (alias) constructor NotFound<{ readonly id: string; }>(args: { readonly id: string; }): NotFound
id- (property) id: string
Errors and custom matchers
An error class matches its own instances, by instanceof, so two classes with the same tag stay distinct. As an extractor it gives the instance: when NotFound(let error):. Any other class matches by instanceof as well.
A value with a [customMatcher] method decides for itself. customMatcher is the proposal's Symbol.customMatcher:
// run: Console.log([parity(2), parity(3)].join(", "))
const Even = {
[customMatcher](subject: unknown) {
return typeof subject === "number" && subject % 2 === 0
}
}
function parity(n: number) {
return match (n) {
when Even: "even";
default: "odd";
}
}
// Output: even, oddTypes in this example
Even- const Even: { [customMatcher](subject: unknown): boolean; }
customMatcher- const customMatcher: typeof customMatcherThe proposal's
Symbol.customMatcher: a static method that decides whether a subject matches. It returns a boolean forx is M, andfalse,true, or the extracted values for an extractor patternM(...). subject- (parameter) subject: unknown
subject- (parameter) subject: number
function- function parity(n: number): "even" | "odd"!!
parity- function parity(n: number): "even" | "odd"Plain JavaScript: VibeLang code calls it directly. TypeScript callers see
Effect<"even" | "odd", never, never>. n- (parameter) n: number
For an extractor, the method receives { matchType: "extractor" } as its second argument and returns false or the values to match against the list. Any other value matches when it is equal, with NaN matching NaN.
Combining patterns
and requires both patterns, or accepts either, and not rejects its pattern. A guard, if (condition), tests any expression and sees the bindings before it:
// run: Console.log(JSON.stringify([classify({ status: "failed", attempts: 3 }), classify({ status: "failed", attempts: 1 }), classify({ status: "queued", attempts: 0 }), classify({ status: "done", attempts: 0 }), classify({ status: "cancelled", attempts: 0 })]))
function classify(job: { status: string; attempts: number }) {
return match (job) {
when { status: "failed", attempts: let n } and if (n >= 3): "escalate";
when { status: "failed" }: "retry";
when { status: not ("done" or "cancelled") }: "running";
default: "leave";
}
}
// Output: ["escalate","retry","running","leave","leave"]Types in this example
function- function classify(job: { readonly status: string; readonly attempts: number; }): "escalate" | "leave" | "retry" | "running"!!
classify- function classify(job: { readonly status: string; readonly attempts: number; }): "escalate" | "leave" | "retry" | "running"Plain JavaScript: VibeLang code calls it directly. TypeScript callers see
Effect<"escalate" | "leave" | "retry" | "running", never, never>. job- (parameter) job: { readonly status: string; readonly attempts: number; }
status- (property) status: string
attempts- (property) attempts: number
n- let n: number
and, or, and not cannot share a level: write (a and b) or c, not a and b or c (EK4705). An or or not pattern cannot bind names yet (EK4702).
is
value is pattern is true when the pattern matches. It binds as tightly as instanceof:
// run: Console.log(JSON.stringify([isRetryable(new Timeout()), isRetryable(new Denied({ user: "Ada", reason: "root" })), isRetryable(new NotFound({ id: "7" }))]))
function isRetryable(failure: Failure) {
return failure is Timeout or Denied
}
// Output: [true,true,false]Types in this example
function- function isRetryable(failure: Failure): boolean!!
isRetryable- function isRetryable(failure: Failure): booleanPlain JavaScript: VibeLang code calls it directly. TypeScript callers see
Effect<boolean, never, never>. failure- (parameter) failure: Failure
Failure- (alias) type Failure = Denied | NotFound | Timeout
Timeout- (alias) class Timeout
In the condition of an if or a while, joined by &&, the pattern can bind names. They are in scope in the rest of the condition and in the if's first branch or the loop's body:
// run: Console.log(firstFailure("ada")); Console.log(firstFailure("").includes("NotFound"))
function firstFailure(id: string) {
const outcome = exit(getUser(id))
if (outcome is Exit.Failure(let cause) && Cause.hasFails(cause)) {
return Cause.pretty(cause)
}
return "ok"
}
// Output: ok
// Output: trueTypes in this example
firstFailure- function firstFailure(id: string): string!!
id- (parameter) id: string
outcome- const outcome: Exit<{ readonly name: string; }, NotFound>
exit- const exit: <{
readonly name: string;
}, NotFound, never>(self: {
readonly name: string;
}!NotFound!) => Exit<{
readonly name: string;
}, NotFound>!!Runs here and returns
Exit<{ readonly name: string; }, NotFound>.Transforms an effect to encapsulate both failure and success using theExitdata type. getUser- (alias) function getUser(id: string): {
readonly name: string;
}!NotFound!Passed by name to
exitas a program: it runs when that program runs, not here, and returns{ readonly name: string; }. cause- let cause: Cause<NotFound>
Cause- (alias) module "effect/Cause"
hasFails- const hasFails: <NotFound>(self: Cause<NotFound>) => booleanReturns
trueif the cause contains at least oneFailreason. pretty- const pretty: <NotFound>(cause: Cause<NotFound>) => stringFormats a
Causeas a human-readable string for logging or debugging.
Elsewhere, an is with bindings is EK4703.
Exhaustiveness
A match without a default arm whose value is a finite union must handle every case: literals, null, undefined, error classes, Option, Exit, Result, and other objects with a literal _tag. A missing case is a compile error:
// expect-error: EK4701
type Phase = "draft" | "review" | "published"
function phaseLabel(phase: Phase) {
return match (phase) {
when "draft": "Draft";
when "published": "Published";
}
}Types in this example
Phase- type Phase = "draft" | "published" | "review"
function- function phaseLabel(phase: Phase): "Draft" | "Published"!!
phaseLabel- function phaseLabel(phase: Phase): "Draft" | "Published"Plain JavaScript: VibeLang code calls it directly. TypeScript callers see
Effect<"Draft" | "Published", never, never>. phase- (parameter) phase: Phase
error EK4701: This match does not handle every case of Phase: "review" is left.For any other type, a match without default that no arm fits throws a TypeError when it runs, as the proposal specifies.
Errors
| Code | Cause | Fix |
|---|---|---|
| EK4701 | A match without default misses a case of a finite union | Add an arm for each case left, or a default arm |
| EK4702 | A binding inside an or or not pattern | Bind outside the combinator, or give each alternative its own arm |
| EK4703 | An is with bindings outside an if or while condition | Move the test into the condition, or use match |
| EK4704 | A regular expression pattern, a relational pattern such as < 10, or a computed key other than a name or a literal | Use a guard: and if (n < 10) |
| EK4705 | and, or, and not at one level without parentheses | Add parentheses |
Not yet supported
- Array patterns match arrays only, not other iterables.
- Object patterns do not match primitives, so
{ length: let n }does not match a string. - An extractor with a custom matcher types its bindings loosely; a class or a listed matcher types them exactly.
isbindings are not in scope in anelsebranch or after the statement.- A subject such as
user.roleis read once per test, not once, so a getter there runs more than once. Assign it to aconstfirst when that matters. - A match inside a larger expression runs its arms in their own function, so an arm there cannot
yield. A match afterreturnor as a statement can. async matchand the proposal's regular expression and relational patterns.