DEV Community

loading...
Cover image for Pattern matching in JavaScript

Pattern matching in JavaScript

maxart2501 profile image Massimo Artizzu ・4 min read

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 (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).

At the current stage, it looks like this:

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: 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!

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:

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 to provide a default value:

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:

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:

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, if you're used to them), you can actually use regular expressions as primitive pattern matchers:

// 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:

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

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?

Discussion (8)

pic
Editor guide
Collapse
savagepixie profile image
SavagePixie

Then real problem here is that it's good just for static values, as the following approach would execute all the expressions:

You could do something like this:

const ACTIONS = {
  save: action => saveThing(action.payload),
  load: action => loadThing(action.payload.id),
  delete: action => deleteThing(action.payload.id)
}
ACTIONS[action.type](action)
Collapse
maxart2501 profile image
Massimo Artizzu Author

Yes indeed! The point is that we have to be careful about this technique because those aren't branches and are eagerly executed.

Wrapping it in a function solves the problem but it's not something we do with if statements, for example 😄

Collapse
pclundaahl profile image
Patrick Charles-Lundaahl

Not sure about performance, but you can swap the immediate invocation for a bind. Eg:

const ACTIONS = {
  save: action => saveThing.bind(this, action.payload),
  load: action => loadThing.bind(this, action.payload.id),
  delete: action => deleteThing.bind(this, action.payload.id)
}
ACTIONS[action.type]()

This way none of your functions get invoked except the one that you explicitly match.

You would probably want to throw a guard condition in there so you don't try to invoke undefined, but this would be my standard approach for anything that needs a dynamic list of functions that have external dependencies.

Collapse
pclundaahl profile image
Patrick Charles-Lundaahl

I'd wager it would be pretty straightforward to build a library to do this. I built something similar to verify json against user-defined schemas. You could extend that idea to check conditions or pass arguments into a callback on success.

Collapse
maxart2501 profile image
Massimo Artizzu Author

There's something about in Lodash:
lodash.com/docs/4.17.11#cond

Collapse
pclundaahl profile image
Patrick Charles-Lundaahl

Neat! I've never actually used lodash, but it looks like it has some really nice utility methods.

Collapse
victorioberra profile image
Collapse
maxart2501 profile image
Massimo Artizzu Author

Oh I hope so!

Alas, it won't be for production projects for a while, but a working Babel plugin is important to make things move forward.