VibeLang

Warning: VibeLang is v0. The language will change in major, breaking ways. It is not suitable for production use.

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

VibeLang
// 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:

VibeLang
// 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 millis

Types 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

PatternMatchesExample
LiteralAn equal string, number, boolean, null, or undefinedwhen 404:
BindingAnything, and names it with let, const, or varwhen let other:
ArrayAn array of that length; ... allows morewhen [let first, ...let rest]:
ObjectA value with those properties; { let id } binds idwhen { status: 200, let body }:
MatcherAn instance of a class, or what a custom matcher acceptswhen NotFound:
ExtractorA matcher's values, matched by the list in parentheseswhen Option.Some(let user):
and, or, notConjunction, alternative, or negationwhen "a" or "e":
GuardA condition that is truewhen [let n] and if (n > 0):
VibeLang
// 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:

MatcherTestExtracts
Option.Some, Option.NoneOption.isSome, Option.isNonethe value
Exit.Success, Exit.FailureExit.isSuccess, Exit.isFailurethe value, the cause
Result.Success, Result.FailureResult.isSuccess, Result.isFailurethe success, the failure
String, Number, Boolean, BigInt, Symboltypeofthe value
VibeLang
// 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: true

Types 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 Option data type represents optional values. An Option<A> is either Some<A>, containing a value of type A, or None, 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 the Exit data type.
getUser
function getUser(id: string): { readonly name: string; }!NotFound!Passed by name to exit as 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 Cause as 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:

VibeLang
// 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, odd

Types 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 for x is M, and false, true, or the extracted values for an extractor pattern M(...).
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:

VibeLang
// 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:

VibeLang
// 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:

VibeLang
// 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: true

Types 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 the Exit data type.
getUser
(alias) function getUser(id: string): { readonly name: string; }!NotFound!Passed by name to exit as 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 true if the cause contains at least one Fail reason.
pretty
const pretty: <NotFound>(cause: Cause<NotFound>) => stringFormats a Cause as 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:

VibeLang
// 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
Output
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

CodeCauseFix
EK4701A match without default misses a case of a finite unionAdd an arm for each case left, or a default arm
EK4702A binding inside an or or not patternBind outside the combinator, or give each alternative its own arm
EK4703An is with bindings outside an if or while conditionMove the test into the condition, or use match
EK4704A regular expression pattern, a relational pattern such as < 10, or a computed key other than a name or a literalUse a guard: and if (n < 10)
EK4705and, or, and not at one level without parenthesesAdd 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.
  • is bindings are not in scope in an else branch or after the statement.
  • A subject such as user.role is read once per test, not once, so a getter there runs more than once. Assign it to a const first when that matters.
  • A match inside a larger expression runs its arms in their own function, so an arm there cannot yield. A match after return or as a statement can.
  • async match and the proposal's regular expression and relational patterns.