DEV Community

Cover image for JavaScript Pattern Matching using 'match' with Flow
George Zahariev
George Zahariev

Posted on • Originally published at Medium

JavaScript Pattern Matching using 'match' with Flow

Branching in JavaScript can be a pain. switch? Full of footguns. Nested conditional ternary expressions? Unreadable. Initializing a let over a bunch of if/else? Verbose. That weird chain of && in your JSX? No thanks. And none of these handle complex structures or check if you handled every case.

If you haven’t looked at Flow recently: its syntax has grown so close to TypeScript that most code is hard to tell apart (here’s a side-by-side for TypeScript users). But Flow isn’t just type annotations: it adds first-class language features built for safety.

match expressions and statements are the newest. They branch on a value: the first pattern that matches maps to a result. match destructures as it matches, and it's exhaustively checked. If you forget a case of the input, Flow tells you what's missing and prevents that bug from hitting production.

An expression, not just a statement

switch is a statement. It can't produce a value, so the moment you want one you're back to a let you assign across cases, or a conditional ternary chain. match is both a statement and an expression. As an expression, it gives you the value directly:

const description = match (action) {
  {type: 'add', const text} => `Add: ${text}`,
  {type: 'toggle', const id} => `Toggle ${id}`,
  {type: 'remove', const id} => `Remove ${id}`,
};
Enter fullscreen mode Exit fullscreen mode

Each pattern matches and extracts values at the same time. {type: 'add', const text} checks that type is 'add' and binds text in one step, no separate destructuring.

Exhaustively checked by default

Drop a case and Flow tells you exactly which one is missing:

type Action =
  | {type: 'add', text: string}
  | {type: 'toggle', id: string}
  | {type: 'remove', id: string};
declare const action: Action;

const description = match (action) {
  {type: 'add', const text} => `Add: ${text}`,
  {type: 'toggle', const id} => `Toggle ${id}`,
  // ERROR: add the missing pattern `{type: 'remove', id: _}`
};
Enter fullscreen mode Exit fullscreen mode

Add a new variant to Action during a later refactor, and every match that doesn't handle it lights up. Delete a variant and Flow points at the now-dead patterns to remove.

A better switch

When you want a statement, match is a switch without the footguns. Cases don't fall through, so there's no break to forget. Each case is its own block, so use those lets and consts without worry. And just like the expression form, it's exhaustively checked.

// Before
switch (command) {
  case 'delete':
  case 'remove':
    data.pop();
    break;
  case 'add':
    data.push(1);
    break;
  default:
    show(data);
}
// After
match (command) {
  'delete' | 'remove' => {
    data.pop();
  }
  'add' => {
    data.push(1);
  }
  _ => {
    show(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

'delete' | 'remove' is an or-pattern: one block for several patterns. _ is the wildcard, matching anything left over.

Patterns go deep

Patterns aren’t limited to single values. They reach into nested objects and arrays, binding as they go:

const name = match (entity) {
  {type: 'user', details: {const name}} => name,
  {type: 'page', pageInfo: {const pageName}} => pageName,
  {type: 'employee', const preferredName} => preferredName,
};
Enter fullscreen mode Exit fullscreen mode

They match arrays and tuples by shape, so you can branch on several values at once. Exhaustiveness works here too: leave a combination out and Flow tells you the fix.

const style = match ([align, position]) {
  ['start', 'above'] => styles.alignTopLeft,
  ['start', 'below'] => styles.alignBottomLeft,
  ['end', 'above'] => styles.alignTopRight,
  // ERROR: add the missing pattern `['end', 'below']`
};
Enter fullscreen mode Exit fullscreen mode

Conditional React rendering, finally readable

Conditional rendering in React is exactly what match expressions are for:

function Page({tab}: {tab: 'home' | 'details' | 'settings'}) {
  return match (tab) {
    'home' => <Home />,
    'details' => <DetailsPane />,
    'settings' => <Settings />,
  };
}
Enter fullscreen mode Exit fullscreen mode

No ternary nest, no stray &&, and if a new tab is added to the union, Flow makes you handle it.

Try it

match brings exhaustiveness, destructuring, and readable branching together in one construct. It's the kind of language-level feature Flow exists to add: not just describing the JavaScript you already write, but giving you better JavaScript to write.

It’s available now.

Top comments (0)