Skip to content
LogoLogo

VIP-7: match is TC39 pattern matching, plus a compile-time exhaustiveness check

Status: accepted by Will, 2026-10-28.

Decision

  • Clarified 2026-11-03: throw arms follow VIP-4. A throw arm fails the Effect in effect code, rejects the Promise in an async match, and is EK0401 in other synchronous VibeLang code. The async-match boundary covers its subject, tests and arms; nested source callables keep their own contexts.
match (e) {
  when NotFound: guest;                // NotFound's Symbol.customMatcher decides, as TC39 says
  when Option.Some(let v): v;
  when DbError and if (retryable): retry();
  default: fallback;
}
  • match, when, is, patterns, guards, and/or/not and default are exactly the TC39 pattern-matching proposal, pinned to a dated snapshot. We move to a newer snapshot deliberately, never automatically.
  • The full proposal is implemented, not today's subset (D207): regular-expression and relational patterns and async match included.
  • One addition: a match with no default over a finite union that misses a case is a compile error. The check only reports errors; it never changes what runs.
  • Class arms use the class's Symbol.customMatcher. VibeLang's error classes get a matcher that tests _tag, so when NotFound: matches across copies of a package.
  • Clarified 2026-11-03: the matcher key. The runtime initialization authorized by VIP-31 defines the global Symbol.customMatcher when absent as Symbol.for("Symbol.customMatcher"), with the descriptors and initialization order in Runtime guarantees. An existing host symbol keeps its identity; own matchers and runtime helpers use that actual key. Native counterparts choose the host key when present and the registry key otherwise. This does not patch Effect or install matcher methods on built-in constructors.
  • Condition: no run-time patching of Effect. Until Effect ships custom matchers for Option, Exit, Result and its tagged errors, the compiler keeps today's fixed checks for them (D208). We propose the matchers upstream.
  • Amended 2026-10-29: what the exhaustiveness check covers. Literal unions, boolean, data unions, error unions and enums. A guarded arm covers nothing. Sent to Will on Telegram 2026-10-29 11:28 under his standing rule 'assume I accept'; no objection by 12:00.
  • Amended 2026-10-29: the retained tagged-error checks. The retained checks for Effect's tagged errors are each module's own guard, as D208 compiles Option.Some to Option.isSome: twelve effect 4.1.1 constructors with a literal _tag and a companion isX guard (Cause.TimeoutError → Cause.isTimeoutError(s), Schema.SchemaError → Schema.isSchemaError(s), and the others listed in the spec's Pattern matching chapter). Each is recognized by resolved binding, aliases included and shadowing excluded. A bare pattern runs the guard; an extractor matches [s]; arity mismatch is a failed match. The subject's static type never chooses the test. Every other constructor uses ordinary class matching (amended below). A VIP-32 Effect version bump regenerates the list. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 53).
  • Amended 2026-10-29: async match in effect code. async match in effect code is a plain async boundary inside its own async arrow, run through an explicit bridge: try promise(() => async match (b) { … }). Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 65).
  • Amended 2026-10-30: class patterns on ordinary classes test instanceof. The pinned draft's inherited Function.prototype matcher reads a [[ConstructedBy]] internal slot that only an engine can record, so compiled JavaScript can't emulate it for classes and objects from untouched .ts or .js code. A class with its own Symbol.customMatcher (VibeLang error, schema error and generated matchers, and the retained Effect checks above) uses that matcher; any other class matches when subject instanceof C, then destructures as the draft says. No helper patches Function.prototype or records construction. So an object whose prototype changed after construction doesn't match its class, and Object.create(C.prototype) does; when engines ship the draft's matcher, VibeLang follows it in a VIP. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 67).
  • Amended 2026-10-30: built-in class patterns. A class pattern naming a built-in uses Array.isArray(subject) for Array and subject instanceof C for every other built-in (Error and its subclasses, AggregateError, Date, RegExp, Map, Set, WeakMap, WeakSet, Promise, typed arrays, ArrayBuffer), because compiled JavaScript can't read the draft's internal slots portably. A cross-realm built-in other than an array fails instanceof. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 67a).
  • Amended 2026-10-30: a class pattern on a VibeLang value that is not a class. A class pattern naming a data declaration or variant constructor, a schema declaration, a service or a Context.Reference is a compile error (EK0702; a service keeps EK3004). Only error declarations and the retained Effect checks have their own Symbol.customMatcher. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 67b).
  • Amended 2026-10-29: which subjects are checked, and which arms cover a case. The subject's type is normalized into union members (boolean to true | false, an enum to its members, a data type to its variants, Option to None | Some, Exit and Result to Success | Failure). The check applies when every member is a literal, null, undefined, an enum member, a data variant, an error or schema error instance, one of those six cases or a retained Effect error; any other member turns it off. Only unguarded arms cover; a bare binding covers everything; an object pattern covers variant V when it has _tag: "V" and its other properties are bare keys or let bindings of required fields; or covers the union; and, not, regex, relational, array, built-in and user-matcher patterns cover nothing. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 78).
  • Amended 2026-10-30: lexical cell identity. Alternatives of one or share the cell of a name bound with the same keyword. A later alternative may replace a cell initialized by a failed earlier alternative; saved closures observe the replacement. Source const assignments remain forbidden; ordinary TDZ and duplicate-initialization rules outside that authorized replacement remain.
  • Amended 2026-10-30: loop-head binding lifetimes. Lexical is bindings in while/do…while shadow outer names in a loop-head scope and never leak outside the loop. Every complete condition evaluation, including after continue, starts with fresh uninitialized cells. A successful condition and its following body share those cells, including later let writes seen by closures; short-circuiting can leave a binding in TDZ even when the condition is true. The initial do body uses uninitialized head cells; closures captured there retain those cells permanently in TDZ. Later conditions never rebind the initial cells. Nested source scopes and ordinary if block scope retain their existing rules.
  • Amended 2026-10-30: the other binding homes, and use before assignment. In for (init; test; update), an init binding belongs to the loop head as a let or const declared in init would, copied into each iteration when it is let; a test or update binding is a fresh cell in each iteration environment. A for…in, for…of, for await, for concurrent or for try head expression's binding is copied into each iteration. A concise arrow body's binding belongs to the arrow body, and a field initializer's binding to that initializer; an enum member initializer has no home, so a binding there is EK0705. is and match bindings take part in TypeScript's definite-assignment and use-before-declaration checks (TS2454, TS2448); a read those checks can't see throws ReferenceError while the cell is uninitialized.
  • Amended 2026-11-03: unbraced statement bodies (replaces the 2026-10-31 amendment). The statement body of if, else, for, for…in, for…of, for await, for concurrent, for try, while, do…while or a labeled statement, when it is not a Block, cannot be the lexical home of an is pattern binding: report EK0705, matching TypeScript's TS1156 for a let declaration in that position. The quick fix and vibelang --fix wrap that statement body in braces. An if head belongs to the home that contains the if statement; an else if head therefore needs braces around the else body when it binds. A binding inside an explicit do block, match arm, callable body or loop head keeps its own home. Will decided this rule on 2026-11-03; it replaces detail 245's implicit-body home.
  • Amended 2026-10-31: one name per home. The pinned duplicate-initialization rules and their or exception apply within one top-level pattern, as the explainer's scope sentence says. Two top-level patterns that bind one name in the same lexical home are a redeclaration, as two let declarations are: TS2451, or TS2300 beside a parameter. Separate blocks, match arms and the other binding homes are separate homes. The diagnostic offers to rename the later binding and its uses, and vibelang --fix applies that rename. This rejects programs the previous statement-local compiler accepted, so the codemod ships with the rule. Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 245).
// compiler-gap: pending VIP-7
function loopReaders() {
  const x = 99; let n = 0; const readers: (() => number)[] = []
  while (n < 2 && (++n is let x)) { readers.push(() => x); x += 10 }
  return [x, ...readers.map(f => f())] // [99, 11, 12]
}
function alternativeReader() {
  let saved: () => number = () => 0
  const matched = [5] is ([let x and if ((saved = () => x, false))] or { length: let x })
  return [matched, saved()] // [true, 1]
}

Native lexical-cell witnesses for these fixed inputs preserve each closure's cell identity:

import { Effect } from "effect"
 
function loopCells() {
  const x = 99
  const readers: (() => number)[] = []
  for (const input of [1, 2]) { let x = input; readers.push(() => x); x += 10 }
  return [x, ...readers.map(f => f())]
}
const loopCellsExit = Effect.runSyncExit(Effect.sync(loopCells)) // Success([99, 11, 12])
let initialReader: () => number = () => 0
initialHead: {
  initialReader = (): number => x
  break initialHead
  const x = 0
}
const initialHeadExit = Effect.runSyncExit(Effect.sync(initialReader)) // Die(ReferenceError); later cells cannot initialize this closed-over binding
 
function alternativeCellWitness() {
  let shared = 5
  const saved = () => shared
  shared = 1
  return [true, saved()]
}
const alternativeCellExit = Effect.runSyncExit(Effect.sync(alternativeCellWitness)) // Success([true, 1])
  • Amended 2026-10-29: static typing. Patterns type as the TypeScript counterpart the specification shows for them: a match has the union of its arm types (a throw arm contributes never), the subject narrows in each arm as the arm's test narrows in TypeScript, and each let binding has the type TypeScript gives the bound value in the counterpart. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 79).

  • Amended 2026-10-29: a lint for extractors that can never match. When a class or fixed pattern's extracted list has a statically known length and an extractor without a rest pattern has a different number of arguments, VibeLang reports a VIP-38 lint warning (EK0703), with the quick fixes when X: and when X(let e):. Matching is unchanged. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 82).

  • Amended 2026-10-29: the complete inventory of retained fixed checks. The retained fixed checks are eighteen: Option.Some, Option.None, Exit.Success, Exit.Failure, Result.Success, Result.Failure, and the twelve tagged errors of the amendment above, each recognized by resolved binding and tested with its module's own guard. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 86).

  • Amended 2026-10-29: primitive class patterns. String, Number, Boolean, BigInt and Symbol patterns test the primitive or its boxed object, typeof s === "string" || s instanceof String, and extract [s] for a primitive and [s.valueOf()] for a boxed object. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 87).

  • Amended 2026-10-29: subpath tagged errors. The retained tagged-error checks cover every public effect/… module, not only the root: 21 more constructors (AiError, McpClientError, CryptoError, CsvError, EncodingError, JsonArrayError, NdjsonError, TarError, XmlError, ZipError, GraphQLClientError, CookiesError, HeaderParseError, HttpBodyError, HttpClientError, HttpServerError, MultipartError, DatagramSocketError, SocketError, SqlError, WorkerError), each tested with its module's isX guard and extracting [s]. The inventory is 39 fixed checks, 33 of them tagged errors. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 92).

  • Amended 2026-11-03: the pinned JoseError. Effect 4.1.1 adds effect/jose, whose JoseError is a tagged error with the guard isJoseError. The 2026-10-29 rule covers every public effect/… module, so this check joins the inventory, tested with its module's guard and extracting [s]: 40 fixed checks, 34 of them tagged errors. This applies that rule to the VIP-32 pin; it is no new personal answer from Will.

  • Amended 2026-10-29: inherited static matchers stay unused. A class pattern uses Symbol.customMatcher only when it is the class's own property. Reading an inherited one would make a .ts class Sub extends NotFound {} match every NotFound, because the generated matcher tests the base error's _tag. Changing this needs an amendment with new evidence. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 93).

  • Amended 2026-10-29: a class pattern naming a non-class callable dispatches on its static signatures. For a callee with no own Symbol.customMatcher that is not a retained fixed pattern, a built-in or a VibeLang value: a construct signature gives subject instanceof C, then the draft's extraction; call signatures only call it as the draft does, C(subject, { matchType }) with its receiver kept, and require a boolean return for a bare pattern and Iterable<unknown> | false for an extractor; any other return type, or a callee with neither signature (any, unknown, Function), is EK0702. A type predicate narrows the subject as if (p(s)) does. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 94).

  • Amended 2026-10-29: an extractor on an ordinary class without its own matcher is EK0704. When a class pattern's callee takes the construct-signature path (no own Symbol.customMatcher, not a retained fixed pattern, built-in or VibeLang declaration), the draft's fallback matcher returns true, so when C(…), with any argument count, () included, can only fail to match or throw TypeError. It is EK0704, with the quick fixes when C: and, when the arguments bind the constructor's parameter properties, when C and { p1: let a, p2: let b }:. An inherited matcher doesn't count. The bare when C: stays s instanceof C, and the run-time rule stays the draft's. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 100).

  • Amended 2026-10-29: exhaustiveness for unions of objects with a literal _tag. A subject member whose type has a _tag property of exactly one string literal type (an object type, an interface or a class instance type) is a checked member, so EK4701 applies when every member is a checked kind; any member without a literal _tag still turns the check off. Coverage is unchanged. Only _tag is a discriminant. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 102).

  • Amended 2026-10-29: an error class pattern narrows as Effect.catchTag does. when NotFound(let e): and when NotFound: narrow to the subject's own members whose _tag is "NotFound" when it has any; only otherwise (unknown, any, a non-union object type) does the binding take the declared class type. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 103).

  • Amended 2026-10-30: leaving a match. However a match or is test ends, its cached iterators close before control goes on, as at the end of a for…of body: a value, a return, a break or a continue closes and goes on, and an error from closing replaces it. A pending throw with a closing error gives the draft's AggregateError. The draft's FinishMatch appends every abrupt completion to its errors, which turns return 7 into throw 7; VibeLang reads that step as "a throw completion", as the draft's notes intend. In effect code a failed try, or a throw, abandons the generator, so nothing closes. A no-match error is new TypeError(). A return in an async match is a syntax error under its explicitly adopted VIP-7 body contexts. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later".

  • Amended 2026-10-30: _tag exhaustiveness counts literal cases. EK4701 counts the literal cases of switch (s._tag) per normalized member. A pattern testing a shared literal covers each member's matching case even when TypeScript does not narrow the whole subject because every member has the same _tag type. A generated error's bare or one-binding pattern likewise covers each member's matching tag case, including a plain object with that tag; any other tag cases on that member remain. A member is tagged when its _tag is a literal type: a string, number, bigint, boolean, enum, null or undefined literal, a unique symbol, or a union of those. Each literal is a case, and an optional _tag has the case undefined, so { _tag?: "A" } | { _tag: "B" } is checked and the absent tag is left, as tsc leaves it. Any other _tag type turns the check off. Members that share a tag are covered one at a time: an object pattern covers a member only when every other property it tests is required on that member, as "x" in s narrows. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later". This replaces "exactly one string literal type" in the amendment above.

  • Amended 2026-10-30: leaving an async match is EK0706. A return that belongs to an async match, or a break or continue whose target is outside it, is EK0706, in the subject, patterns, guards and arm expressions, including inside a do expression. A nested function keeps its own return, and a jump to a target inside the async match follows the ordinary rules. Ownership and targets come from the source, not from the generated async function. This codes the existing syntax error; it admits and forbids nothing new.

  • Amended 2026-10-30: negation follows actual recognition. not P uses only the TypeScript false-branch narrowing of P's actual recognition test. Use !(s instanceof C) only for a dispatch path whose positive test is s instanceof C. Own matchers and predicates negate their invocation; an arbitrary boolean result narrows neither branch. Primitive/built-in adapters, retained Effect guards and generated tagged-error patterns likewise complement their actual tests. This adds no exhaustiveness coverage for negation.

Negation complements the actual recognition test for every dispatch path: literals use their equality test, ordinary classes their instanceof test, primitive/built-in adapters their specified tests, retained Effect patterns their module guards, generated tagged errors their tag tests, and own matchers or predicates their invocation. Only the false-branch narrowing established by that TypeScript test applies. An arbitrary boolean-returning matcher establishes no narrowing on either branch. This typing rule adds no exhaustiveness coverage for not.

// compiler-gap: pending VIP-7
class OnlyNumbers {
  readonly kind = "instance"
  static [Symbol.customMatcher](s: unknown, _hint: { matchType: "boolean" }): boolean {
    return typeof s === "number"
  }
}
function matcherComplement(s: OnlyNumbers | number) {
  return match (s) { when not OnlyNumbers: s; default: "number"; }
}
// Replacing the negated arm result with s + 1 is TS2365: the matcher does not narrow s.
class Plain { readonly kind = "plain" }
function ordinaryComplement(s: Plain | number) {
  return match (s) { when not Plain: s + 1; default: 0; }
}
import { Effect, Exit } from "effect"
 
const complementMatcher: unique symbol =
  ((Symbol as typeof Symbol & { readonly customMatcher?: symbol }).customMatcher ?? Symbol.for("Symbol.customMatcher")) as any
class OnlyNumbers {
  readonly kind = "instance"
  static [complementMatcher](s: unknown, _hint: { matchType: "boolean" }): boolean {
    return typeof s === "number"
  }
}
function matcherComplement(s: OnlyNumbers | number) {
  if (!OnlyNumbers[complementMatcher](s, { matchType: "boolean" })) return s
  return "number"
}
function rejectedComplement(s: OnlyNumbers | number) {
  if (!OnlyNumbers[complementMatcher](s, { matchType: "boolean" })) {
    // @ts-expect-error TS2365: a boolean-returning matcher establishes no narrowing
    return s + 1
  }
  return 0
}
class Plain { readonly kind = "plain" }
function ordinaryComplement(s: Plain | number) {
  if (!(s instanceof Plain)) return s + 1
  return 0
}
const instance = new OnlyNumbers()
const matcherExit = Effect.runSyncExit(Effect.sync(() => [
  matcherComplement(instance) === instance,
  matcherComplement(7),
  ordinaryComplement(new Plain()),
  ordinaryComplement(7)
]))
console.log(matcherExit) // Success([true, "number", 0, 8])
  • Amended 2026-10-30: async results and default flow use TypeScript's actual tests. For arm-type union U, async match has type Promise<Awaited<U>>, with native async-return adoption and TypeScript's Promise-like/thenable checks. A returned Promise is adopted rather than nested. A default arm follows TypeScript control flow after the preceding ordered pattern and guard tests fail, including cached reads and source mutations. This is independent of exhaustiveness coverage: a predicate can narrow its false branch without covering a case, and a failed guard does not establish that its positive predicate failed. Do not subtract a coverage set instead of analyzing those tests.
// compiler-gap: pending VIP-7
function isText(s: string | number): s is string { return typeof s === "string" }
function classifyDefault(s: string | number) {
  return match (s) { when isText: s.length; default: String(s); }
}
function guardedDefault(s: string | number) {
  return match (s) {
    when isText and if (false): 0;
    default: typeof s === "string" ? s.length : s;
  }
}
const adoptedResult: Promise<number> = async match (true) { default: Promise.resolve(7); }
import { Effect, Exit } from "effect"
 
function isText(s: string | number): s is string { return typeof s === "string" }
function classifyDefault(s: string | number) {
  if (isText(s)) return s.length
  return String(s)
}
function guardedDefault(s: string | number) {
  if (isText(s) && false) return 0
  // @ts-expect-error the failed guard leaves both string and number possible
  const numberOnly: number = s
  return typeof s === "string" ? s.length : s
}
const adoptedResult: Promise<number> = (async () => Promise.resolve(7))()
const resultFlowExit = await Effect.runPromiseExit(Effect.gen(function* () {
  return [
    classifyDefault("text"),
    classifyDefault(7),
    guardedDefault("text"),
    guardedDefault(7),
    yield* Effect.promise(() => adoptedResult)
  ]
}))
console.log(resultFlowExit) // Success([4, "7", 4, 7, 7])
  • Amended 2026-10-30: formal-parameter binding homes. An is test that would introduce a let/const binding into formal-parameter initialization is EK0705: the pinned explainer explicitly leaves that lexical home unfinished. This includes direct/destructured defaults and evaluated computed binding keys. Binding-free tests remain allowed; explicit source blocks and match arms inside an initializer keep their own local bindings, and nested source callable bodies keep their boundaries. Ordinary call arguments in a lexical block use that block. Preserve eager once-only initialization, parameter/body scope separation, length, TDZ, closures and exception boundaries; no generic move-to-body repair is supplied. This conservatively rejects an unspecified scope rather than introducing parameter-binding machinery.

  • Amended 2026-10-31: limit and catch-parameter homes. A binding in a for concurrent limit belongs to the nearest home that encloses the loop, because the limit runs before the head environment exists. A binding in catch-clause parameter initialization is EK0705, as for a formal parameter: that environment is distinct from the catch Block, and no reconciliation gives it a binding home. Binding-free tests and explicit source blocks stay allowed. Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 249).

  • Amended 2026-10-30: remaining pattern-syntax diagnostics. Invalid match/is grammar and proposal-specific early errors without a more specific assigned diagnostic use EK0707. An interpolated primitive template pattern uses that code; write the ordinary interpolation in a guard. Do not duplicate a more specific diagnostic for the same rejection. Independent native lexical/type/name errors and errors in valid subexpressions keep TypeScript's diagnostics, including a malformed native template. Diagnostic ownership follows the source rule even when the parser reuses TypeScript helpers; this changes no admitted program.

  • Amended 2026-10-30: generic and refined subject domains. Begin coverage normalization with the TypeScript type of the subject expression at its evaluation point, including represented flow refinements, and use its effective base constraint when one exists. Finite constrained parameters are checked conservatively over that constraint; unconstrained or broad parameters stay open. A represented intersection refinement may reduce the domain; a bare type parameter keeps its whole constraint even after a guard. Preserve literal, Boolean, enum and tag-case identity rather than boxing every subject with apparent-type conversion. This changes neither runtime tests nor ordinary arm/default-flow types.

  • Amended 2026-10-31: a bare fixed pattern covers its case. when Option.Some: covers Some, as when Option.Some(let v): does, and likewise bare Option.None, Exit.Success, Exit.Failure, Result.Success and Result.Failure: the extracted-length rule applies only to a written argument list, and no list is not an empty list. Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 78).

  • Amended 2026-10-30: explicit async-match body contexts. The accepted Promise-producing sketch uses [~Yield, +Await, ~Return, ~Effect] for its subject, tests and arms. await is allowed; a thrown exception rejects the Promise; return and outward break/continue are syntax errors. A nested source callable retains its own contexts. These are the snapshot's explicit reconciliations; its vendored specification has no formal async-match production to supply them.

Binding homes and redeclarations

The following rules specify binding homes under the adopted proposal.

Two top-level patterns that bind one name in the same lexical home are a redeclaration, as two let declarations are, and report what such a let reports: TS2451 beside another lexical declaration, TS2300 beside a parameter.

The statement body of if, else, for, for…in, for…of, for await, for concurrent, for try, while, do…while or a labeled statement, when it is not a Block, cannot be the lexical home of an is pattern binding: report EK0705, matching TypeScript's TS1156 for a let declaration in that position. The quick fix and vibelang --fix wrap that statement body in braces. An if head belongs to the home that contains the if statement; an else if head therefore needs braces around the else body when it binds. A binding inside an explicit do block, match arm, callable body or loop head keeps its own home.

A binding in a for concurrent limit belongs to the nearest home that encloses the loop, because the limit runs before the head environment exists; it is one cell, not copied into items.

An is test in formal-parameter initialization is EK0705 if a let or const pattern binding would need the parameter environment as its lexical home. This includes direct defaults, defaults nested in parameter destructuring, and evaluated computed binding keys. The same holds for a catch clause's parameter, whose environment is distinct from the catch Block. The pinned explainer leaves that scope unfinished, so no parameter-home binding is invented or moved into the body.

Meaning in Effect TS terms

match is plain JavaScript, not an Effect: TC39's semantics, with the arms tested in order and the subject evaluated once. It compiles to ordered tests on the subject, with no Effect involved.

Corrected 2026-10-29: the example ends every match arm with ;, the last one included, as the pinned TC39 grammar requires; a newline is not a separator (found by the round-3 review).

History

ProblemKindStatus
Structural error types (D501) made instanceof arms unsound for exhaustivenesslego, majorfixed (D515), kept here by custom matchers
The subject's static type chose tag test vs instanceoflego, majorfixed (D531), kept here by custom matchers
or patterns: chapters disagreed on what runslocal, minorfixed (D558), now TC39's definition
Guard grammar disagreed between grammar, chapters and parserlocal, minorfixed (D502), TC39's and if
A local named Option still compiled to Option.isSome (D208)local, none raisedopen until Effect ships matchers
The formatter broke a statement match after a call (ASI)implementationfixed

No reviewer raised a match issue in round 11.

Prior art

The TC39 pattern-matching proposal; Effect's Match.exhaustive; Rust's and Swift's exhaustive match and switch.

Will's words

"Ok I accept it."

Footnote: old VibeLang

Old VibeLang is a reference only; nothing in it is part of the language unless a VIP adds it. For implementers, this VIP replaces:

D207's subset. D206, D209, D515 and D531 are restated here on TC39's terms.

  • Amended 2026-11-01: project options and extraction presence. Pattern bindings have their specified TypeScript counterpart's types under the project's options; the strict examples do not freeze those types under exactOptionalPropertyTypes or strictNullChecks. Proven element/key presence prevents noUncheckedIndexedAccess from adding artificial undefined; it does not remove actual payload undefined. Pattern keys are not dotted reads under noPropertyAccessFromIndexSignature. Written indexed and dotted reads keep those options. A printed counterpart carries extraction presence in its types, for example with a checked iterator result; a raw array-length guard alone is not that proof.