# Simpler pattern matching proposal

**URL:** <https://es.discourse.group/t/simpler-pattern-matching-proposal/1963>\
**Category:** 🦋 Proposals\
**Created:** [March 23, 2024, 3:05pm UTC](https://es.discourse.group/t/simpler-pattern-matching-proposal/1963 "2024-03-23T15:05:42Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![phaux](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/phaux/32/2036_2.png) [@phaux](https://es.discourse.group/u/phaux)\
**Post date:** [March 23, 2024, 3:05pm UTC](https://es.discourse.group/t/simpler-pattern-matching-proposal/1963/1 "2024-03-23T15:05:42Z")

</div>

I wrote this alternative proposal to maybe inspire somebody to take the pattern matching proposal in a different direction. I initially wanted to post it in github issues, but looks like they're busy with rewriting the proposal once again and I don't want to spam.

Problems with the existing pattern matching proposal:

- Doesn't interact with existing patterns. Invents new pattern syntax with completely different semantics.
- Adds separate constructs which use only the new pattern syntax while leaving existing ones without the new featurs.
- Can't be split into separate proposals. Most of features are tightly coupled with each other.
- Probably is never gonna get accepted because it's so big.

# ECMAScript Simple Pattern Matching Proposal

Let's add pattern matching to ECMAScript by introducing a few small features and syntax additions.  
Every feature presented here adds a bit of useful functionality which can be used on its own and **can even be split into a separate proposal**.

The general idea is to

1. Take existing patterns (let/const bindings, function parameters, etc) and expand their capabilities by allowing new kinds of pattern syntax.
2. Add a new syntax to the `switch` statement which matches using any pattern (in addition to the existing `case` statement which matches by a single value)

## Already existing pattern syntax

There's already some pattern syntax which asserts the type of a value and otherwise throws an error. This syntax would work seamlessly with new `switch` additions. Consistency!

### Object pattern

Asserts that the value is an object, otherwise throws an error.

```js
const { a, b: c, d = 5, ...rest } = obj;

```

### Array/iterable pattern

Asserts that the value is iterable, otherwise throws an error.

```js
const [a, b, c = 3, ...rest] = arr;

```

## New pattern syntax

The following are additions to the pattern syntax. They will work in existing places where patterns are used and in the new `switch` syntax. All new syntax is just an example and alternative syntax can be proposed.

### Literals in patterns

Allow literals in patterns which act like a value assertion. If the matched value isn't equal to the one provided in the pattern an error is thrown. Allowed are string, number and boolean values and null.

```js
// Throws an error if the response was not ok or status wasn't 200
const { ok: true, status: 200, statusText } = await fetch("/");

// Throws an error if the first segment of path isn't "root"
const ["root", ...rest] = path.split("/");

// Simply assert that status is "success", otherwise throw
const "success" = status

```

> TODO: currently an `undefined` literal would instead try to bind the value to a variable named `undefined`. It fails in most cases, but works in function parameters. Possible solution: keyword `void` matches `undefined`.

### Not-undefined pattern

Checking for property existence is one of the most common applications of pattern matching. Unfortunately the current behavior of patterns is that when a property is missing, the whole pattern still matches and the binding is set to undefined. This necessitates a syntax which would change that behavior. It should be concise since it probably will be used often.

Let's add a pattern which asserts that the value is not undefined. The syntax shall be a unary postfix `!` after a sub-pattern.

It changes the subpattern so that it only matches if the matched value is not undefined. In other words, it adds an additional requirement to the pattern, so it works kinda like and-pattern automatically (see below).

A shorthand for destructured object properties shall be supported.

```js
// Throws an error if the function isn't called with a user ID
function getUser(userId!) {
  // Will throw early if user's name or age is missing.
  const { name: firstName!, age! } = users[userId];
}

```

### Or pattern

Add a pattern which matches if at least one of the sub-patterns match. If one subpattern matches, the rest aren't evaluated. If none of the sub-patterns match, an AggregateError is thrown.

The syntax shall be a binary `|` operator where operands are sub-patterns.

If a variable binding exists in only one of the sub-patterns, it will be declared, but will be undefined if other sub-pattern matches.

If the same iterable is iterated in multiple sub-patterns, it will be iterated only once. The result of the iteration will be cashed and reused in the other sub-patterns.

```js
// Throws an error if the response wasn't successful.
const { status: 200 | 201 } = await fetch("/");

// Retrieves user's name from either first or last name property.
// Throws if neither is defined.
const { firstName: name! } | { lastName: name! } = user;

```

## New `switch` syntax

Add new syntax to the `switch` statement to make use of the pattern syntax.

### `const` and `let` cases

Allow `const` and `let` in addition to `case` and `default` in `switch` statements.

The statement matches if the given pattern doesn't result in one of the errors described above. Only the errors originating from failed patterns are caught and treated as failed match. If, for example, an object getter throws a custom exception during match, it won't be caught automatically.

If an iterable is iterated in multiple cases, it will be iterated only once and it's results will be cached and reused in the other cases.

```js
switch (response) {
  const { ok: true }: 
    console.log(await response.json());
    break;
  const { ok: false, status, statusText }: 
    console.error(`Error: ${status} ${statusText}`);
    break;
}

```

> TODO: is this distinguishable from a regular const or let statement? Maybe add a keyword: `case const` and `case let`?

### `break`less cases

Add a syntax similar to an arrow function which allows omitting the `break` statement. The syntax shall use thin arrow (`->`) to distinguish it from a function syntax, but also to suggest similar usage.

```js
switch (payload) {
  const { type: "add", value } -> state.push(value);
  const { type: "remove", index } -> state.splice(index, 1);
  const { type: "update", index, value } -> {
    state[index] = value;
  }
}

```

> TODO: Can semicolons be optional?

### `switch` as an expression

Either wait until the [do-expressions proposal](https://github.com/tc39/proposal-do-expressions) is implemented or propose a similar feature just for switch. The syntax should align with the do-expressions proposal.

Let's assume that all you have to do is put a switch in expression position to enable this behavior.

```jsx
return (
  <div>
    {switch (request) {
      const { status: "loading" } -> <Spinner />;
      const { status: "success", data } -> <UserProfile data={data} />;
      const { status: "error", error } -> <Alert message={error} />;
    }}
  </div>
);

```

## Summary

These additions encompass most of the features of the popular [`ts-pattern`](https://github.com/gvergnaud/ts-pattern) library.

One advantage of bringing these features to the language is that they additionally allow control flow (like `break` and `return`) to work from inside the match branches.

> **Possible future improvements**
>
> These are just ideas which could be added in the future proposals or not.
> 
> ### And pattern
> 
> Add a pattern which matches if all of the sub-patterns match. If at least one of the sub-patterns fails, the whole pattern fails and throws an error.
> 
> ```js
> // Asserts that the options parameter is an object
> function init(options & {}) {}
> 
> // Asserts that the array has exactly 2 elements and binds them to x and y
> const { length: 2 } & [x, y] = point;
> 
> ```
> 
> ### `let` and `const` in `if` and `while` statements
> 
> Allow `let` and `const` in `if` and `while` statements to test patterns and bind variables at the same time.
> 
> [Similar feature](https://doc.rust-lang.org/reference/expressions/if-expr.html#if-let-expressions) exists in Rust.
> 
> ```js
> if (const { status: 200, statusText } = await fetch("/logout", { method: "POST" })) {
> console.log("Success:", statusText);
> }
> 
> ```
> 
> > TODO: What about `if (let pat = expr && let pat = expr)`? Rust doesn't support it neither, but makes it a syntax error for future compatibility.
> 
> ### Pattern guards
> 
> Allow syntax at the end of any pattern which allows to add any condition to the pattern. The syntax shall be `pattern if (expr)`.
> 
> [Similar feature](https://doc.rust-lang.org/reference/expressions/match-expr.html#match-guards) exists in Rust.
> 
> ```js
> switch (response) {
> const { status } if (status >= 200 && status < 300) -> console.log("Success");
> const { status } if (status >= 400 && status < 500) -> console.warn("Client error");
> default -> console.error("Unexpected response");
> }
> 
> ```
> 
> TODO: Should it be allowed in `if-let` and `if-const` as well? How about regular `let` and `const`?
> 
> ### `typeof` pattern
> 
> A pattern which allows matching on value's type. The syntax shall be a prefix `typeof` operator followed by a sub-pattern which matches the string value representing the type: `typeof <pattern>`
> 
> ```js
> switch (value) {
> const typeof "string" -> console.log("It's a string");
> const typeof "number" -> console.log("It's a number");
> default -> console.log("It's something else");
> }
> 
> function add(a & typeof "number", b & typeof "number") {
> return a + b;
> }
> 
> // Assigns `typeof value` to `valueType` in a weird way
> const typeof valueType = value
> 
> ```
> 
> ### `instanceof` pattern
> 
> A pattern which asserts that the value is an instance of a given class. The syntax shall be a prefix `instanceof` operator followed by an expression which evaluates to a class: `instanceof <expr>`.
> 
> ```js
> try {
> doSomething()
> } 
> catch (error) {
> switch (error) {
> const instanceof TypeError & { message } -> console.error("Type error:", message);
> const instanceof RangeError & { message } -> console.error("Range error:", message);
> default -> {
> throw error;
> }
> }
> }
> 
> ```
> 
> ### Comparison patterns
> 
> A pattern which asserts that the value is greater, less or equal to a given value. The syntax shall be a prefix comparison operator followed by an expression. E.g. `<= 123`.
> 
> ```js
> switch (response.status) {
> const >= 500 -> console.error("Server error");
> const >= 400 -> console.warn("Client error");
> default -> console.log("Success");
> }
> 
> const { length: > 0 & <= 10 } = array;
> 
> ```
> 
> ### Template literal pattern
> 
> A pattern which asserts that the value is a string which matches a given template literal. The holes in the literal are sub-patterns.
> 
> ```js
> const `${user}@${domain}` = email;
> 
> switch (userCommand) {
> const `/kick ${userId}` -> kickUser(userId);
> const `/ban ${userId}` -> banUser(userId);
> }
> 
> ```
> 
> ### RegExp pattern
> 
> A pattern which asserts that the value is a string which matches a given regular expression.
> 
> ```js
> switch (s) {
> const /.*@.*\..*/ -> console.log("It's an email");
> const /.*:\/\/.*/ -> console.log("It's a URL");
> }
> 
> ```
> 
> ### Let-else and const-else statements
> 
> Add a syntax to `let` and `const` which allows to do something else than throwing a default error. Variable declarations can be followed by an `else` branch which runs when the pattern doesn't match. The `else` branch can be a single diverging statement or a block which ends with a diverging statement (`return`, `break`, `continue` or `throw`).
> 
> [Similar feature](https://doc.rust-lang.org/reference/statements.html#let-statements) exists in Rust.
> 
> ```js
> for (const child of element.children) {
> const { tagName: "a", href } = child else continue;
> console.log("Link:", href);
> }
> 
> ```
> 
> ### Exact length iterable pattern
> 
> Default array/iterable pattern currently only matches on the specified starting elements and ignores the rest. There could be a syntax which makes the pattern fail if there are more elements than expected.
> 
> ```js
> // Must have at least 1 element and no more than 3
> const [a!, b, c, ...void] = generator();
> 
> ```
> 
> ### Custom matchers
> 
> Add `@expr` syntax which evaluates expr and calls it's `Symbol.match` method with the value. If it throws an error, the pattern fails.
> 
> ```js
> if (const { length: @range(0, 10) } = str) {}
> 
> ```
> 
> > If the match method returns a value, it could be matched by a sub-pattern. Syntax is TODO.

---

<div class="post-metadata">

**Author:** ![ljharb](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/ljharb/32/8_2.png) [@ljharb](https://es.discourse.group/u/ljharb)\
**Post date:** [March 23, 2024, 7:52pm UTC](https://es.discourse.group/t/simpler-pattern-matching-proposal/1963/2 "2024-03-23T19:52:54Z")

</div>

A few points after a quick skim:

- Object destructuring doesn't assert it's an object, it asserts it's non-nullish - it only throws on null or undefined
- a trailing ! would conflict syntactically with TypeScript, but more importantly, conceptually it wouldn't match the intuitive usage which is that it's null OR undefined, and in function arguments, there's three possibilities - "absent" is distinct from "undefined".
- in `const { status: 200 | 201 }`, there's a few problems. one is that this would only allow for patterns that are not identifiers, and another is that `|` conceptually overlaps too much with the bitwise OR operator.
- `switch` is a horrifically unintuitive and bug-prone construct and I would personally be opposed to any improvement of it. It needs a complete replacement, which is what the current proposal is.

---

<div class="post-metadata">

**Author:** ![phaux](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/phaux/32/2036_2.png) [@phaux](https://es.discourse.group/u/phaux)\
**Post date:** [March 23, 2024, 9:01pm UTC](https://es.discourse.group/t/simpler-pattern-matching-proposal/1963/3 "2024-03-23T21:01:30Z")

</div>

> [@ljharb](#):
>
> Object destructuring doesn't assert it's an object

I think it's fine and it should work the same way in pattern matching.

> [@ljharb](#):
>
> a trailing ! would conflict syntactically with TypeScript

Seems like my examples with this syntax currently fail to parse on ASTexplorer.

Also, your previous point made me realize that this syntax could also be `pattern & {}`. In the end I don't care which syntax will be choosen anyways.

> [@ljharb](#):
>
> conceptually it wouldn't match the intuitive usage which is that it's null OR undefined

I conceptualized `{ foo! }` as shorthand for something like `{ foo = throw MatchError() }` (aka shorthand for default value where the value is a throw expression). That's why I thought it should reject only undefined, but it's not that important to me.

> [@ljharb](#):
>
> this would only allow for patterns that are not identifiers

I rarely ever needed such a pattern and I used a lot of ts-pattern library and Rust.

Also it could be added later as `@var` or `==var`. See hidden "possible future improvements" in OP.

> [@ljharb](#):
>
> `|` conceptually overlaps too much with the bitwise OR operator

Might as well be `||` or `or` like in the current proposal. I just think `|` looks nicest and it overlaps with TS union syntax which is good.

> [@ljharb](#):
>
> `switch`

I agree. I just tried to come up with something that would require minimal additions to the language.

---

<div class="post-metadata">

**Author:** ![bergus](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/bergus/32/152_2.png) [@bergus](https://es.discourse.group/u/bergus)\
**Post date:** [March 23, 2024, 10:54pm UTC](https://es.discourse.group/t/simpler-pattern-matching-proposal/1963/4 "2024-03-23T22:54:36Z")

</div>

> [@ljharb](#):
>
> a trailing ! would conflict syntactically with TypeScript

TypeScript doesn't allow non-null assertions in destructuring patterns. It seems logical, and [the feature was requested](https://github.com/microsoft/TypeScript/issues/17390), but it's currently rejected

---

<div class="post-metadata">

**Author:** ![bergus](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/bergus/32/152_2.png) [@bergus](https://es.discourse.group/u/bergus)\
**Post date:** [March 24, 2024, 12:07am UTC](https://es.discourse.group/t/simpler-pattern-matching-proposal/1963/5 "2024-03-24T00:07:16Z")

</div>

I love this proposal, the syntax feels quite natural!

Some quibbles and ideas:

- "_If an iterable is iterated in multiple cases, it will be iterated only once and it's results will be cached and reused in the other cases._" - that will be potentially awkward if the patterns have different lengths. You'd need to keep the iterators open until a matching pattern is found, and only the close all iterators that were opened during any of the previous matching attempts. It might be simpler to iterate the iterable value multiple times. Just like a getter should be evaluated multiple times when the same property is matched by multiple patterns.
- Literal patterns should allow bigints and `undefined`
- Literal patterns for symbols are impossible :-(
- As @ljharb indicated, `& {}` has the same meaning as `!`
- `const typeof "string"` reads weird. I get its usefulness in other patterns, but you should've chosen an example that cannot be simplified to `switch (typeof value) { case "string" -> …`
- `const instanceof RangeError & { message } = error` doesn't read well. Can we have `const { message } instanceof RangeError = error;`?
- template literal patterns seem too powerful - and at the same time not powerful enough for parsing, since they can only do `.startsWith` and `.indexOf`. Maybe rather wait for custom matchers or extractors.

However, I wonder mostly how these patterns work with the full destructuring syntax with aliases and default initialisers.

- Would it be `const { message: msg! } = error` or `const { message!: msg } = error`?
- How can I still extract the property value into a variable after matching it with literal pattern alternatives? For objects I could do `const { status: 200 | 201, status } = response`, but for iterable destructuring it seems impossible.
- Would default initialisers be evaluated before or after matching the result value against a pattern? I presume that (with current evaluation order) `const { status: 200 = 200 } = response` would match an object where the `.status` property is `undefined`.

Maybe it would be prudent to use a guard/matcher syntax for everything more complicated than literal patterns (where creating a binding isn't useful anyway). I could imagine something like

```js
const { name: firstname @!=(null), age @!=(null) } = users[userId];
const { status @===(200, 201) } = response; // still binds `status` variable
const [x, y] @{ length: 2 } = point;
const { status } @if(status >= 200 && status < 300) = response;
const myString @typeof("string") = value;
const myNumber @typeof("number") = value;
const myStringOrNumber @typeof("string", "number") = value;
const { message } @instanceof(TypeError, RangeError) = error;
const { status @>=(500) } = response;
const { length @>(0) @<=(10) } = array;
const email @matches(/.*@.*\..*/) = s;
const { length @inRange(0, 10) } = str;
const [x, y] @hasLength(2) = point;

```

Basically use

- `@` with relational operator (including `typeof` and `instanceof`) and a set of arguments to test for any of them to match
- `@if` with boolean expressions to match arbitrary conditions (with the prior bindings already in scope)
- `@` with arbitrary identifier to run a user-defined function (which returns boolean value)
- `@` with another object pattern or array pattern to apply a second pattern - _not sure if useful_

Any such `@`-pattern should be usable in the grammar

- after any other target pattern (`x @…`, `[x @…]`, `[x = def @…]`, `[x @… = def]`, `{x: y @…}`, `{x: y = def @…}`, `{x: y @… = def}`)
- including in a chain with multiple `@`-patterns (`x @… @…`) where all will need to match
- even in place of any target pattern, leading to no binding being created (`@…`, `[@…]`, `{x: @…}`) - although that would collide with potential syntax for extractors.

---

<div class="post-metadata">

**Author:** ![phaux](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/phaux/32/2036_2.png) [@phaux](https://es.discourse.group/u/phaux)\
**Post date:** [March 24, 2024, 2:53am UTC](https://es.discourse.group/t/simpler-pattern-matching-proposal/1963/6 "2024-03-24T02:53:48Z")

</div>

> [@bergus](#):
>
> "_If an iterable is iterated in multiple cases, it will be iterated only once and it's results will be cached and reused in the other cases._" - that will be potentially awkward if the patterns have different lengths.

This is how the original proposal works and I think it's fine.

> [@bergus](#):
>
> Literal patterns should allow bigints and `undefined`

`undefined` is already allowed in patterns so it wouldn't be backwards compatible.

> [@bergus](#):
>
> Would it be `const { message: msg! } = error` or `const { message!: msg } = error`?

The first one. The latter one would be syntax error.  
`{ message! }` is just a shorthand for `{ message: message! }`.

> [@bergus](#):
>
> How can I still extract the property value into a variable after matching it with literal pattern alternatives? For objects I could do `const { status: 200 | 201, status } = response`, but for iterable destructuring it seems impossible.

You could do `{ status: status & (200 | 201) }` if the and-pattern will be included in the proposal. I'm striving for something minimal tho.

> [@bergus](#):
>
> Would default initialisers be evaluated before or after matching the result value against a pattern?

Good question. I think default initializers must apply first and then the inner pattern must match on the possibly defaulted value. That's because you can already write a pattern like `{ options: { foo } = { foo: "bar" } }`.

> **More on custom matchers**
>
> > [@bergus](#):
> >
> > Maybe it would be prudent to use a guard/matcher syntax for everything more complicated than literal patterns
> 
> I included the `@customMatcher` syntax only because people really want it for some reason. I personally never needed much more than what's in my main proposal. Your examples show very well why I don't like it. This syntax looks very alien and not like JS at all.
> 
> Also with my extra proposal the exact syntax would be `{ prop: variable & @foo & @bar }` and the ones that use a keyword don't need the `@` because they are different kinds of patterns.
