ðŸŠī\zansh.in\blog

3 min read

Pattern Matching in JavaScript

Pattern-matching is something I've leaned-on a lot recently, and I'm starting to anticipate its inclusion in the language more and more. It's currently a Stage 1 proposal.

Maybe to help it along I can share this little library I've been tinkering with.

I've tried to, ahem, match the TC39 spec as closely as I could in anticipation of the hopeful day when I don't need to use it anymore:

import { match, otherwise, when } from 'match-iz'
let result = match(data)(
when(pattern, result || handler),
when(pattern, result || handler),
otherwise(result || handler)
)

# Why not if / switch?

If pattern-matching is new to you, it's essentially a declarative version of if and switch, where you describe the expected shape of your data using "patterns".

Patterns are a combination of both functions and data, and because of this certain assumptions can be made to help reduce the amount of boilerplate normally required to check that your data looks a certain way:

// Imperative:
if (typeof res?.statusCode === 'number') {
if (res.statusCode >= 200 && res.statusCode < 300) {
return res.body
}
}
// Declarative:
return match(res)(
when({ statusCode: inRange(200, 299) }, () => res.body),
otherwise(() => {})
)

You can view more imperative vs. declarative examples over on the match-iz README.md.

# How does it work?

Let's expand a little on the above example.

const res = {
statusCode: 200,
body: 'Hello, world!'
}

We want to check the object res has a statusCode prop with a value between 200 and 299 inclusive, and we want to use another object to help us do that, like this one:

const matcher = {
statusCode: value => {
return typeof value === 'number' && value >= 200 && value <= 299
}
}

To use the matcher to check res in a naive and hard-coded way, we can do something like this:

const key = 'statusCode'
const valueToCheck = res[key]
const valueChecker = matcher[key]
const foundMatch = Object.hasOwn(res, key) && valueChecker(valueToCheck)

match-iz basically does this for us: It will look at all the keys of matcher to see if they are contained in the res object. For any that exist, it will then check to see if the value within the corresponding matcher is either literal, or a function, and use that to determine if the res value is a match:

return match(res)(
when({ statusCode: inRange(200, 299) }, () => res.body),
otherwise(() => {})
)

inRange (a pattern helper) is essentially returning a function that works the same way as the one we see in matcher.statusCode. It looks a bit like this:

function inRange(fromInclusive, toInclusive) {
return valueToCheck => {
return (
typeof valueToCheck === 'number' &&
valueToCheck >= fromInclusive &&
valueToCheck <= toInclusive
)
}
}

The match/when combination are compiling and running such matchers.

# In the Wild

I've used match-iz myself for these other little projects: sift-r and viddy.

If you'd like to try a different tool, there are many other similar libraries) for adding pattern-matching to your JavaScript.

18 Aug, 2021

Google it:

ðŸŒļ first-class functions 🌚 imperative vs declarative programming 🌷 inclusive exclusive ranges ðŸŒŧ object hasown 🌞 object literal ðŸŒđ typeof 💐 pattern matching