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.
Table of Contents
- 1. Why switch/case is not enough
- 2. Basic syntax: the match expression and when clauses
- 3. Structural patterns: testing objects and arrays
- 4. Guards: extra conditions in when clauses
- 5. Matchlets: binding and reusing sub-patterns
- 6. Type patterns: instanceof and typeof in pattern matching
- 7. Exhaustiveness and the default clause
- 8. Pattern matching vs. switch/case vs. an if-else chain
- 9. Using it today: the Babel plugin and a polyfill strategy
- 10. Summary
- 11. FAQ
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.