Pattern matching in JavaScript
Tags: javascript, patternmatching
Published: 2020-01-14 17:15:55

Pattern matching is a pretty common action performed on entities in order to check if they follow some *pattern* or not.

For example, an object representing an animal could have a `wings` property or not, and thanks to its mere presence (out a value greater than 0) we can know which of them are birds or other flying critters.

This technique is useful *per se*, but *particularly* in dynamically typed languages, as they can't easily discriminate against class names if we're dealing with generic structures. But it turns out most dynamically typed languages do *not* have native pattern matching.

And JavaScript is perhaps the most common dynamically typed language. Let's see what's the situation there.

## TC39 pattern matching proposal

And just as predicted, JavaScript doesn't have native pattern matching. But in the future things might change. But there is a [proposal](https://github.com/tc39/proposal-pattern-matching) (currently at stage 1 of the process) that aims to introduce pattern matching in JavaScript. When it'll reach stage 4 it will be soon ratified as part of the ECMAScript language (some of you know ["JavaScript" is copyrighted by Oracle](https://tsdr.uspto.gov/#caseNumber=75026640&caseType=SERIAL_NO&searchType=statusSearch)).

At the current stage, it looks like this:

```js
const res = await fetch(jsonService);
case (res) {
  when {status: 200, headers: {'Content-Length': s}} ->
    console.log(`size is ${s}`),
  when {status: 404} ->
    console.log('JSON not found'),
  when {status} if (status >= 400) -> {
    throw new RequestError(res)
  }
}
```
It's quite clear how this syntax would help with the old and trite task of [duck typing](https://en.wikipedia.org/wiki/Duck_typing): we can check for the existence of multiple properties/methods at once, and expresso conditions about their value. It also gives us the benefits of [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)!

Unfortunately, this proposal is still on stage 1, and has been like that since late May 2018. This means it could take a while before it will reach stage 3 (when vendors would probably start implementing the proposal), let alone stage 4... *if* it will reach those stages.

So let's have a look at what we can do for pattern matching in JavaScript *today*.


## Just `switch`

The good ol' `switch` statement provides basic pattern - or better, *value* matching. JavaScript's `switch` is unfortunately pretty weak, providing just comparison by strict equivalence, and a `default` branch:

```js
let statusText;
switch (statusCode) {
  case 200:
    statusText = 'Ok';
    break;
  case 404:
    statusText = 'Not found';
    break;
  case 500:
    statusText = 'Internal server error';
    break;
  default:
    statusText = 'Unknown error';
}
```

Since JavaScript has `case` statement fallthrough, you can also match against multiple values, but that more often than not is a source of bugs for missing `break` statements.


## Value mapping

The simplest form of pattern matching is also the weakest. It's nothing more than using a key/value pair to find the corresponding value. You can also short-circuit with `||` or use the new [nullish coalescing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) to provide a default value:

```js
const STATUS_TEXTS = {
  200: 'Ok',
  404: 'Not found',
  500: 'Internal server error'
};
const statusText = STATUS_TEXTS[statusCode] ?? 'Unknown error';
```

This is basically as weak as `switch`, but surely it's more compact. Then real problem here is that it's good just for static values, as the following approach would execute *all* the expressions:

```js
const ACTIONS = {
  save: saveThing(action.payload),
  load: loadThing(action.payload.id),
  delete: deleteThing(action.payload.id)
};
ACTIONS[action.type]; // ... and?
```

At this point the "thing" has been saved, loaded and deleted... and maybe not even in this order!


## Regular expressions

Well yeah, regular expressions *are* a way to pattern-match stuff! The bad news is that it works with just strings:

```js
if (/^\d{3} /.test(statusError)) {
  console.log('A valid status message! Yay!');
}
```

The good news is that `.test` doesn't throw if you pass something different than a string, and it would also call its `.toString` method beforehand! So, as long as you provide a way to serialize your objects (like in [snapshot testing](https://jestjs.io/docs/en/snapshot-testing), if you're used to them), you can actually use regular expressions as *primitive* pattern matchers:

```js
// Checks if object has a statusCode property with a 3-digit code
if (/"statusCode":\d{3}\b/.test(JSON.stringify(response)) {
  console.log(`Status code: ${response.statusCode}`);
}
```

The *ugly* news is that it's a rather obscure technique that basically none uses, so... Maybe don't? 😅


## Supercharged `switch`!

The following is maybe the most mind-blowing 🤯

We can use a neat trick with `switch` so we can use whatever test we want, instead of just equality comparisons! But how?!

Have a look at this:

```js
let statusGroup = 'Other'; // default value
switch (true) {
  case statusCode >= 200 && statusCode < 300:
    statusGroup = 'Success';
    break;
  case statusCode >= 400 && statusCode < 500:
    statusGroup = 'Client error';
    break;
  case statusCode >= 500 && statusCode < 600:
    statusGroup = 'Server error';
    break;
}
```

The trick here is providing `true` as the comparison value. At runtime, those `case` statements become all like `case false`, except the one that becomes `case true`: and that gives our match.

I think this is very clever, but it has its downsides. First of all, you can't use `default` anymore, as you'd deal with just `true` or "not `true`". (Also, matching against `true` is just a convention: the expression after `case` may give whatever value after all.)

But above all, just like many "clever" techniques, it's also quite unexpected and a real WTF-generator. And as we all know, the quality of code is measured in WTFs/min:

![The only valid measurement of code quality: WTFs/minute](https://img-9gag-fun.9cache.com/photo/aDWyGyx_700b.jpg)

So, yeah... do that if you want to mess with your peers, but don't ship code like that!

---

I bet many of you folks have used the object mapping, but have you ever used one of the above alternative techniques?