JavaScript Pattern Matching Proposal: the match Expression and Structural Matching
AI generated
JS
() =>
JavaScript · TC39 · Stage 2 Proposal
JavaScript Pattern Matching Proposal
The match Expression and Structural Pattern Matching

switch/case only handles primitive equality, nested conditions quickly become unreadable, and there is no expression character at all. The TC39 Pattern Matching Proposal changes that: a match expression checks structure, type, shape, and value of an object at the same time, declaratively, exhaustively, and without a fallthrough bug.

10 min read match · when · matchlets · guards · structural patterns TC39 Stage 2 · Babel plugin available

1. Why switch/case is not enough

The switch statement in JavaScript has three fundamental weaknesses that constantly force workarounds in everyday code. First, switch only checks primitive equality with a strict === comparison: no structure, no types, no array shape. Second, switch is a statement, not an expression: you cannot directly assign its "return value" to a variable or use it inside a ternary. Third, cases without an explicit break fall through, which in practice often leads to silent bugs. All of this makes pattern matching in JavaScript a long-awaited addition.

The TC39 Pattern Matching Proposal has been in active development since 2017 and reached Stage 2 in 2024, a milestone that means the committee acknowledges the underlying problem and is actively working on the specification. Inspired by pattern matching in Rust (match), OCaml, and Haskell, it brings structural matching directly into JavaScript: a single match expression can check object shape, array length, type instance, and primitive values at the same time, in readable, declarative syntax without nested if chains.

2. Basic syntax: the match expression and when clauses

The match expression in JavaScript follows a clear pattern: match (subject) { when (pattern) => expression }. The subject is evaluated exactly once and then checked against each when clause from top to bottom. The first clause whose pattern matches provides the result of the entire match expression. Because match is an expression, its value can be assigned directly to a variable, returned from an arrow function, or passed as an argument, without a helper variable or an IIFE.

The simplest pattern is a primitive value: when (42) matches when the subject is exactly 42. Several values can be combined in one clause with the or operator: when (1 or 2 or 3). A _ clause or a final when clause without a pattern acts as the default case, which always matches. If no pattern matches and there is no default, the match expression throws a MatchError, an explicit failure instead of the silent undefined that switch returns without a default.


// Basic match expression, evaluates to a value (expression, not statement)
const status = "pending";

const label = match (status) {
  when ("pending")   => "Pending";
  when ("shipped")   => "Shipped";
  when ("delivered") => "Delivered";
  when ("cancelled") => "Cancelled";
  // Default: MatchError if no match matches, forces exhaustive handling
  when (_)           => "Unknown status";
};

console.log(label); // "Pending"

// match is an expression, usable directly in JSX/template literals
const message = `Order is ${match (status) {
  when ("pending")   => "still being processed";
  when ("shipped")   => "already on the way";
  when ("delivered") => "arrived";
  when (_)           => "in an unknown state";
}}`;

// Multiple values per clause with 'or'
const isActive = match (status) {
  when ("pending" or "shipped") => true;
  when (_)                      => false;
};

3. Structural patterns: testing objects and arrays

The most powerful feature of the Pattern Matching Proposal is structural pattern matching: an object pattern like { type: "error", code: 404 } matches any object that contains at least these properties with these values. The subject can have further properties; the pattern only checks the defined keys. Array patterns like [first, ...rest] check the array shape while simultaneously binding parts of the array to variables, identical to destructuring, but with the added semantics of matching.

Nested patterns are fully supported: { user: { role: "admin" }, action: "delete" } checks the outer structure and a deeply nested value at the same time, in a single when expression. Without pattern matching, this would require a chain of && operators and optional chaining that has to be manually adjusted every time the data structure changes. Structural matching makes these conditions declarative and maintainable.


// Structural pattern matching, object and array patterns
const response = { status: 404, body: { message: "Not Found" }, headers: {} };

const result = match (response) {
  // Object pattern: checks shape, binds variables
  when ({ status: 200, body: { data } })  => `Success: ${JSON.stringify(data)}`;
  when ({ status: 404, body: { message } }) => `Not found: ${message}`;
  when ({ status: 500 })                  => "Server error, please retry";
  when ({ status: s }) if (s >= 400)     => `Client error: ${s}`;
  when (_)                               => "Unknown response";
};

// Array pattern: check form and destructure simultaneously
const parseCommand = (args) => match (args) {
  when ([])                          => "No arguments";
  when (["help" or "--help"])        => "Show help";
  when (["--file", filename])        => `Read file: ${filename}`;
  when (["--output", out, ...rest])  => `Output to ${out}, extra: ${rest}`;
  when ([cmd, ...params])            => `Command: ${cmd} with ${params.length} params`;
};

console.log(parseCommand(["--file", "data.json"])); // "Read file: data.json"
console.log(parseCommand(["build", "src", "dist"])); // "Command: build with 2 params"

4. Guards: extra conditions in when clauses

Guards are optional boolean conditions added after the pattern of a when clause with the keyword if. A pattern can match structurally, but the guard can still reject the clause, in which case matching continues with the next clause. Guards are especially useful for numeric comparisons (if (n > 0 && n < 100)), length checks, or complex boolean logic that cannot be expressed through structural patterns alone.

Inside a guard, all variables bound in the pattern are available. A pattern like { amount } binds the value of the amount property, and the guard if (amount > 1000) can use it directly. This combination of structural pattern and boolean guards makes pattern matching the most expressive control flow mechanism in JavaScript, far more powerful than switch/case, without sacrificing readability.

5. Matchlets: binding and reusing sub-patterns

Matchlets are named sub-patterns, defined as const matchlet = pattern, that can be reused across multiple when clauses. They enable the DRY principle (Don't Repeat Yourself) in pattern matching: instead of repeating the same complex sub-pattern in five different clauses, it is defined once as a matchlet and referenced everywhere. This improves readability and turns changes to a frequently used sub-pattern into a one-line edit.

A matchlet for an error structure could look like this: const isError = { type: "error" }. In the when clauses it is used as when ({ ...isError, code: 404 }) or when (isError). The Pattern Matching Proposal thereby enables composition of patterns at the same level as composition of functions, a paradigm that brings substantial maintainability gains, especially in TypeScript projects with complex union types.

6. Type patterns: instanceof and typeof in pattern matching

The Pattern Matching Proposal supports type checks directly in the pattern. With instanceof Error as a pattern, the clause matches any instance of the Error class and its subclasses. With typeof "string", it matches any string. These type patterns can be combined with structural patterns: instanceof TypeError { message } matches a TypeError object and simultaneously binds the message property. This makes error handling and type dispatch considerably more readable than a chain of if (err instanceof TypeError) blocks.

In TypeScript projects, pattern matching interacts particularly well with the discriminated union pattern. A union type type Shape = Circle | Rectangle | Triangle can be fully covered in a single match expression, with TypeScript checking exhaustiveness and providing the correct type narrowing information for every branch. This replaces the common switch (shape.kind) pattern with string literals with a type-safe, structural match.


// Type patterns: instanceof and typeof in match clauses
function handleError(err) {
  return match (err) {
    when (instanceof TypeError { message })
      => `Type error: ${message}`;
    when (instanceof RangeError { message })
      => `Range error: ${message}`;
    when (instanceof Error { message, stack })
      => `Generic error: ${message}`;
    when (typeof "string")
      => `String error: ${err}`;
    when (_)
      => `Unknown error: ${String(err)}`;
  };
}

// Discriminated union pattern (TypeScript)
// type Action =
//   | { type: "increment"; amount: number }
//   | { type: "decrement"; amount: number }
//   | { type: "reset" }

function reducer(state, action) {
  return match (action) {
    when ({ type: "increment", amount }) => state + amount;
    when ({ type: "decrement", amount }) => state - amount;
    when ({ type: "reset" })             => 0;
    // TypeScript knows all cases are covered, no default needed
  };
}

7. Exhaustiveness and the default clause

A key advantage of pattern matching over switch/case is the explicit error behavior for missing cases. If no pattern matches and there is no default clause (when (_)), the match expression throws a MatchError with a helpful error message. This is the opposite of the silent undefined that switch returns without a default. In production applications, an explicit error on unexpected input is always better than a silent failure that only surfaces hours later through downstream effects.

In TypeScript, the exhaustiveness of pattern matching interacts with the type system: if all cases of a union type are covered, the compiler recognizes that the when (_) default case is unreachable and warns when noUnusedLocals is enabled. If a new case is added to the union type and the match expression has no default, the compiler raises an error, the same behavior that has been a major contributor to program correctness in Rust and Haskell for decades.

Feature switch/case if-else chain match (Pattern Matching)
Is an expression No No Yes
Structural matching No Manual Yes, native
Exhaustiveness check No MatchError No MatchError MatchError on missing case
Fallthrough risk Yes (forgetting break) No No
Guards possible No Yes Yes, with if guard

9. Using it today: the Babel plugin and a polyfill strategy

The Pattern Matching Proposal is not yet part of the ECMAScript standard; it is at Stage 2. For production projects, a Babel plugin is available that transforms match syntax into compatible ES2022 JavaScript. The plugin @babel/plugin-proposal-pattern-matching (or corresponding community variants) already enables the use of pattern matching in React, Node.js, and TypeScript projects today. The output code is fully compatible with all modern JavaScript engines.

Alternatively, the library ts-pattern offers a complete pattern matching implementation for TypeScript as a library, without Babel, without a transpiler plugin, using only regular function calls. It supports structural patterns, guards, exhaustiveness checking, and type narrowing, and is an excellent way to use pattern matching productively today, even though the native match expression has not yet landed in the language. For new projects, ts-pattern is recommended as a bridge until native support arrives.


// ts-pattern library, Pattern Matching available today in TypeScript
import { match, P } from "ts-pattern";

type ApiResponse =
  | { status: "success"; data: unknown }
  | { status: "error";   code: number; message: string }
  | { status: "loading" };

function handleResponse(response: ApiResponse): string {
  return match(response)
    // Structural pattern with variable binding
    .with({ status: "success", data: P.select() },
          (data) => `Data: ${JSON.stringify(data)}`)

    // Guard: only match if code is 4xx
    .with({ status: "error", code: P.number.between(400, 499), message: P.select() },
          (msg) => `Client error: ${msg}`)

    .with({ status: "error", code: 500 },
          () => "Server error")

    .with({ status: "loading" },
          () => "Loading…")

    // exhaustive() throws if a case is not covered, compile-time AND runtime safety
    .exhaustive();
}

Mironsoft

TypeScript architecture and modern JavaScript development

Want complex conditional logic that is readable and maintainable?

We migrate nested if-else chains and fragile switch blocks to ts-pattern or native pattern matching, with exhaustive coverage and TypeScript type safety.

Code review

Analysis of switch/if complexity and identification of pattern matching candidates

Migration

Gradual introduction of ts-pattern or a Babel plugin into existing projects

Training

Pattern matching workshop for TypeScript teams covering structural patterns and guards

10. Summary

The JavaScript Pattern Matching Proposal brings a fundamental improvement to control flow expressiveness in JavaScript with the match expression. As a true expression, match can be assigned directly to a variable or used as a return value. Structural patterns check object shape, array length, and type instance at the same time, without nested if chains. Guards add boolean extra conditions to structural patterns. Matchlets allow sub-patterns to be reused. And MatchError makes non-exhaustive coverage immediately visible.

Anyone who wants to use pattern matching in JavaScript today has two options: the Babel plugin for native match syntax, or the ts-pattern library for a type-safe implementation without a transpiler. Both approaches are production ready and prepare the codebase optimally for the day when match is natively supported in all engines.

JavaScript Pattern Matching, the essentials at a glance

match expression

Is an expression, not a statement. Result can be assigned directly. MatchError on non-exhaustive coverage instead of a silent undefined.

Structural patterns

Check object shape, array length, and type instance at the same time. Nesting possible. Guards for boolean extra conditions.

Use it today

Babel plugin for native syntax or the ts-pattern library for TypeScript. Both production ready and future proof.

TC39 status

Stage 2 since 2024. Active specification work. Native browser support expected one to two years after Stage 4.

11. FAQ: JavaScript Pattern Matching Proposal

1What is the JavaScript Pattern Matching Proposal?
TC39 Stage 2 proposal, introduces a match expression for structural pattern matching. Checks object shape, array length, type instance, and primitive values declaratively.
2Difference between match and switch?
match is an expression, switch is a statement. match: structural patterns, guards, MatchError. switch: only primitive equality, fallthrough risk without break.
3What is a MatchError?
An error for non-exhaustive coverage without a default clause. Explicit failure instead of a silent undefined, better for production.
4What are guards?
Optional boolean conditions: when ({ amount }) if (amount > 1000). Pattern must match and guard must evaluate to true. Otherwise the next clause is checked.
5Usable in production today?
Yes: a Babel plugin for native syntax or the ts-pattern library without a transpiler. Both production ready and future proof.
6What is ts-pattern?
A TypeScript library for pattern matching without a transpiler. Supports structural patterns, guards, exhaustiveness, and type narrowing as regular functions.
7What are matchlets?
Named, reusable sub-patterns. DRY principle for pattern matching, define a sub-pattern once, reference it multiple times.
8When is native browser support coming?
Stage 2 since 2024. Realistically one to three years away, after Stage 3 and 4. Available today: the Babel plugin or ts-pattern.
9Pattern matching with TypeScript discriminated unions?
Ideal. match covers all union variants, TypeScript checks exhaustiveness and narrows the type correctly in every branch. Replaces switch (shape.kind).
10Regular expressions as patterns?
Not yet finalized in the current proposal. ts-pattern already supports P.string.regex() today as a RegExp pattern.