Goodbye if-else, Hello Pattern Matching in JavaScript (PROPOSAL)
JavaScript is constantly evolving to provide developers with more concise and powerful ways to write code. One exciting new proposal is pattern matching, which aims to replace cumbersome if-else statements with a more declarative syntax. In this article, we’ll go from a basic overview to some more advanced use cases of pattern matching in JavaScript.
Photo by Gabriel Heinzer on Unsplash
What is Pattern Matching?
Pattern matching allows you to match against the shape and contents of a value in a single expression. It provides a more concise alternative to chained if-else statements and switch cases.
The basic syntax in JavaScript is:
let result = match(value) {
pattern1 => result1,
pattern2 => result2,
// …
}
This matches value against the provided patterns, executes the result expression for the first match, and returns the result.
Basic Use Cases
Let’s look at a simple example of replacing if-else with pattern matching:
// Before
function httpStatus(statusCode) {
if (statusCode >= 200 && statusCode < 300) {
return ‘success’
} else if (statusCode >= 400 && statusCode < 500) {
return ‘client error’
} else if (statusCode >= 500) {
return ‘server error’
}
}
// With pattern matching
function httpStatus(statusCode) {
return match(statusCode) {
[200..299] => ‘success’,
[400..499] => ‘client error’,
500.. => ‘server error’
}
}
The match expression checks the statusCode argument against the provided patterns and returns the associated result expression.
Pattern matching allows us to replace those if-else blocks with a much more readable single expression.
Some key things to note:
Patterns can be property names, literals, ranges, wildcards and more.
The => denotes the result expression to return if the pattern matches.
Patterns are checked sequentially until one matches.
The result of the matching pattern is returned.
This can greatly simplify conditional logic and make intentions clearer.
Matching Literals and Variables
Patterns can match literal values:
function test(x) {
return match(x) {
‘foo’ => ‘x was foo’,
‘bar’ => ‘x was bar’,
_ => ‘x was something else’
}
}
test(‘foo’) // ‘x was foo’
test(‘bar’) // ‘x was bar’
test(‘baz’) // ‘x was something else’
The _ acts as a wildcard pattern to match anything else.
Patterns can also match variables:
function firstElement(arr) {
return match(arr) {
[first, ..rest] => first,
[] => undefined
}
}
firstElement([1, 2, 3]) // 1
firstElement([]) // undefined
This destructures the array to match against the first element and bind it to a variable.
Matching Objects
One very useful feature is matching against object properties:
function handleResponse(response) {
return match(response) {
{ status: 200 } => ‘success’,
{ status: 404 } => ‘not found’,
{ status: 500 } => ‘server error’
}
}
This allows matching the shape of objects, rather than specific identity.
You can also capture properties into variables:
function handleResponse(response) {
return match(response) {
{ status: 200, body } => `Got successful response: ${body}`,
{ status } => `Error ${status} occurred`
}
}
This destructures the status and body properties into variables to use in the result expressions.
Nested Matching
Pattern matching can be nested to handle more complex cases:
function parseCommand(command) {
return match(command) {
{ type: ‘add’, payload: {x, y} } => x + y,
{ type: ‘subtract’, payload: {x, y} } => x — y,
{ type, payload } => match(payload) {
{x, y} => `Unknown command ${type} with ${x} ${y}`,
_ => `Unknown command ${type}`
}
}
}
The nested match expression handles the payload property in more detail.
Matching Types
You can match the type of a value using the is operator:
function stringifyValue(val) {
return match(val) {
value is String => `${value}`,
value is Number => String(value),
value is Boolean => String(value),
_ => ‘unknown value type’
}
}
This can replace complicated typeof checks.
Default Case
It’s good practice to provide a default catch-all case as the last pattern:
function handleStatus(status) {
// … pattern cases
_ => ‘unknown status’
}
This avoids having to check for unexpected cases before the match.
Combining Techniques
All these techniques can be combined together for full control:
function parseUser(user) {
return match(user) {
{ active: true, id, name, email } =>
`Active user: ${id} — ${name} <${email}>`,
{ active: false, id, name } =>
`Inactive user: ${id} — ${name}`,
_ => ‘Unknown user’
}
}
This showcases matching object properties, DE structuring variables, and a default case.
Why Not switch Statements?
You may be wondering why we need pattern matching when JavaScript already has switch statements.
There are a few advantages of pattern matching over switch:
More concise syntax without break keywords
Match full objects, not just primitive values
Flexible pattern types like ranges and wildcards
Variables can be DE structured from patterns
Default case handled automatically
Overall, pattern matching provides a much more declarative and intention-revealing syntax.
Browser Support
Pattern matching is currently a Stage 1 ECMAScript proposal, meaning the syntax is still being worked on and not available in browsers yet.