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:
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?
Top comments (8)
You could do something like this:
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 😄Not sure about performance, but you can swap the immediate invocation for a
bind
. Eg: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.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.
There's something about in Lodash:
lodash.com/docs/4.17.11#cond
Neat! I've never actually used lodash, but it looks like it has some really nice utility methods.
Coming soon to babel: github.com/babel/babel/pull/9318
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.