Skip to content
Codeloom
JavaScript

JavaScript Pattern Matching: switch(true) and the TC39 Proposal

Explore pattern matching in JavaScript today with switch(true), and preview the TC39 pattern matching proposal for cleaner conditional logic.

·7 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • Using switch(true) for multi-condition matching today
  • Destructuring-based pattern matching patterns
  • The TC39 pattern matching proposal and its syntax

Prerequisites

  • JavaScript conditionals and switch statements
  • Destructuring syntax

Pattern matching is a powerful technique for branching based on the structure and content of data. While JavaScript does not yet have a dedicated pattern matching syntax, you can achieve similar results today with switch(true), destructuring, and utility functions. A formal TC39 proposal is also in progress.

The problem with if/else chains

Complex conditionals quickly become hard to read:

function describeResponse(response) {
  if (response.status === 200 && response.data && response.data.length > 0) {
    return `Success: ${response.data.length} items`;
  } else if (response.status === 200 && (!response.data || response.data.length === 0)) {
    return "Success but no data";
  } else if (response.status === 404) {
    return "Not found";
  } else if (response.status >= 500) {
    return "Server error";
  } else {
    return "Unknown response";
  }
}

This is verbose and the conditions are hard to scan at a glance.

switch(true): pattern matching today

switch(true) evaluates each case as a boolean expression. It reads like a pattern match.

function describeResponse(response) {
  const { status, data } = response;

  switch (true) {
    case status === 200 && data?.length > 0:
      return `Success: ${data.length} items`;
    case status === 200:
      return "Success but no data";
    case status === 404:
      return "Not found";
    case status >= 500:
      return "Server error";
    default:
      return "Unknown response";
  }
}

console.log(describeResponse({ status: 200, data: [1, 2, 3] }));
// "Success: 3 items"
console.log(describeResponse({ status: 404 }));
// "Not found"

Each case is a predicate. The first one that evaluates to true wins. This is much more scannable than nested if/else.

Matching shapes with destructuring

Combine destructuring with switch(true) to match object shapes.

function handleEvent(event) {
  const { type, payload } = event;

  switch (true) {
    case type === "USER_LOGIN" && !!payload?.userId:
      return `User ${payload.userId} logged in`;

    case type === "USER_LOGOUT":
      return "User logged out";

    case type === "MESSAGE" && payload?.text?.length > 0:
      return `Message: "${payload.text.slice(0, 50)}"`;

    case type === "ERROR" && payload?.code >= 500:
      return `Server error: ${payload.message}`;

    case type === "ERROR":
      return `Client error: ${payload?.message || "unknown"}`;

    default:
      return `Unhandled event: ${type}`;
  }
}

console.log(handleEvent({ type: "USER_LOGIN", payload: { userId: 42 } }));
// "User 42 logged in"

console.log(handleEvent({ type: "ERROR", payload: { code: 503, message: "Timeout" } }));
// "Server error: Timeout"

Building a match utility function

You can create a reusable match function that makes the pattern more explicit.

function match(value, patterns) {
  for (const [predicate, handler] of patterns) {
    if (predicate(value)) {
      return handler(value);
    }
  }
  throw new Error(`No pattern matched for: ${JSON.stringify(value)}`);
}

const result = match({ type: "circle", radius: 5 }, [
  [(s) => s.type === "circle", (s) => Math.PI * s.radius ** 2],
  [(s) => s.type === "rectangle", (s) => s.width * s.height],
  [(s) => s.type === "triangle", (s) => 0.5 * s.base * s.height],
]);

console.log(result); // 78.53981633974483

Type-checking variant

function matchType(value) {
  switch (true) {
    case value === null:
      return "null";
    case value === undefined:
      return "undefined";
    case Array.isArray(value):
      return `array(${value.length})`;
    case value instanceof Date:
      return `date(${value.toISOString()})`;
    case value instanceof RegExp:
      return `regex(${value.source})`;
    case typeof value === "object":
      return `object(${Object.keys(value).length} keys)`;
    case typeof value === "function":
      return `function(${value.length} params)`;
    default:
      return `${typeof value}(${value})`;
  }
}

console.log(matchType([1, 2, 3]));    // "array(3)"
console.log(matchType({ a: 1 }));     // "object(1 keys)"
console.log(matchType(new Date()));   // "date(2026-07-01T...)"
console.log(matchType(42));           // "number(42)"

Dispatch tables with Map

For simple value-based matching, a Map or object dispatch table is clean and fast.

const handlers = new Map([
  ["start", () => console.log("Starting...")],
  ["stop", () => console.log("Stopping...")],
  ["restart", () => {
    handlers.get("stop")();
    handlers.get("start")();
  }],
]);

function dispatch(command) {
  const handler = handlers.get(command);
  if (!handler) {
    throw new Error(`Unknown command: ${command}`);
  }
  return handler();
}

dispatch("restart");
// "Stopping..."
// "Starting..."

The TC39 pattern matching proposal

The TC39 pattern matching proposal introduces a match expression with structural pattern matching. Here is what the proposed syntax looks like:

// PROPOSED SYNTAX -- not yet available in any runtime
const result = match (response) {
  when ({ status: 200, data: [...items] }) if (items.length > 0):
    `Success: ${items.length} items`;
  when ({ status: 200 }):
    "Success but no data";
  when ({ status: 404 }):
    "Not found";
  when ({ status }) if (status >= 500):
    "Server error";
  default:
    "Unknown response";
};

Key features of the proposal:

  • Structural matching — match against object shapes and destructure simultaneously
  • Guards — add conditions with if clauses
  • Array patterns — match array structures like [first, ...rest]
  • Expression formmatch returns a value (it is an expression, not a statement)

More proposed patterns

// PROPOSED SYNTAX
const describe = (value) => match (value) {
  when (null): "null";
  when (undefined): "undefined";
  when (Number.isFinite): "finite number";
  when ([_, _, _]): "array of exactly 3";
  when ({ x, y }): `point(${x}, ${y})`;
  default: "something else";
};

Emulating the proposal today with a builder

You can get close to the proposed syntax using a builder pattern.

class Matcher {
  #patterns = [];

  when(predicate, handler) {
    this.#patterns.push({ predicate, handler });
    return this;
  }

  otherwise(handler) {
    this.#patterns.push({ predicate: () => true, handler });
    return this;
  }

  run(value) {
    for (const { predicate, handler } of this.#patterns) {
      if (predicate(value)) {
        return typeof handler === "function" ? handler(value) : handler;
      }
    }
    throw new Error("No pattern matched");
  }
}

const classify = (value) =>
  new Matcher()
    .when((v) => typeof v === "number" && v > 0, "positive number")
    .when((v) => typeof v === "number" && v < 0, "negative number")
    .when((v) => typeof v === "number", "zero")
    .when((v) => typeof v === "string", (v) => `string of length ${v.length}`)
    .otherwise("unknown")
    .run(value);

console.log(classify(42));      // "positive number"
console.log(classify(-3));      // "negative number"
console.log(classify("hello")); // "string of length 5"

Regular expression matching

Regex is a natural fit for pattern matching on strings.

function parseCommand(input) {
  switch (true) {
    case /^\/help$/i.test(input):
      return { command: "help" };
    case /^\/set\s+(\w+)\s+(.+)$/i.test(input): {
      const [, key, value] = input.match(/^\/set\s+(\w+)\s+(.+)$/i);
      return { command: "set", key, value };
    }
    case /^\/list\s*(\d*)$/i.test(input): {
      const [, count] = input.match(/^\/list\s*(\d*)$/i);
      return { command: "list", count: count ? parseInt(count) : 10 };
    }
    default:
      return { command: "unknown", raw: input };
  }
}

console.log(parseCommand("/set theme dark"));
// { command: "set", key: "theme", value: "dark" }

console.log(parseCommand("/list 25"));
// { command: "list", count: 25 }

Practical example: state machine reducer

Pattern matching shines for reducers and state machines.

function reducer(state, action) {
  const { type, payload } = action;

  switch (true) {
    case type === "ADD_ITEM" && !!payload?.item:
      return { ...state, items: [...state.items, payload.item] };

    case type === "REMOVE_ITEM" && typeof payload?.index === "number":
      return {
        ...state,
        items: state.items.filter((_, i) => i !== payload.index),
      };

    case type === "SET_FILTER":
      return { ...state, filter: payload?.filter || "all" };

    case type === "RESET":
      return { items: [], filter: "all" };

    default:
      return state;
  }
}

Summary

  • switch(true) is the most practical pattern matching tool available in JavaScript today. Each case is a boolean predicate.
  • Combine with destructuring to match object shapes and extract values.
  • Dispatch tables (Map or object) work well for simple value-based routing.
  • A builder pattern can emulate the feel of structured pattern matching.
  • The TC39 proposal aims to add a match expression with structural patterns, guards, and array matching.
  • Until the proposal lands, switch(true) combined with destructuring covers most use cases cleanly.

Pattern matching makes complex branching logic more readable and maintainable. Even without native syntax support, the patterns shown here will improve the clarity of your conditional code.