• Stars
    star
    5,341
  • Rank 7,340 (Top 0.2 %)
  • Language
    HTML
  • License
    MIT License
  • Created almost 7 years ago
  • Updated 5 days ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Pattern matching syntax for ECMAScript

ECMAScript Pattern Matching

Status

Stage: 1

Spec Text: https://tc39.github.io/proposal-pattern-matching

Authors: Originally Kat Marchán (Microsoft, @zkat__); now, the below champions.

Champions: (in alphabetical order)

Table of Contents

Introduction

Problem

There are many ways to match values in the language, but there are no ways to match patterns beyond regular expressions for strings. switch is severely limited: it may not appear in expression position; an explicit break is required in each case to avoid accidental fallthrough; scoping is ambiguous (block-scoped variables inside one case are available in the scope of the others, unless curly braces are used); the only comparison it can do is ===; etc.

Priorities for a solution

This section details this proposal’s priorities. Note that not every champion may agree with each priority.

Pattern matching

The pattern matching construct is a full conditional logic construct that can do more than just pattern matching. As such, there have been (and there will be more) trade-offs that need to be made. In those cases, we should prioritize the ergonomics of structural pattern matching over other capabilities of this construct.

Subsumption of switch

This feature must be easily searchable, so that tutorials and documentation are easy to locate, and so that the feature is easy to learn and recognize. As such, there must be no syntactic overlap with the switch statement.

This proposal seeks to preserve the good parts of switch, and eliminate any reasons to reach for it.

Be better than switch

switch contains a plethora of footguns such as accidental case fallthrough and ambiguous scoping. This proposal should eliminate those footguns, while also introducing new capabilities that switch currently can not provide.

Expression semantics

The pattern matching construct should be usable as an expression:

  • return match { ... }
  • let foo = match { ... }
  • () => match { ... }
  • etc.

The value of the whole expression is the value of whatever clause is matched.

Exhaustiveness and ordering

If the developer wants to ignore certain possible cases, they should specify that explicitly. A development-time error is less costly than a production-time error from something further down the stack.

If the developer wants two cases to share logic (what we know as "fall-through" from switch), they should specify it explicitly. Implicit fall-through inevitably silently accepts buggy code.

Clauses should always be checked in the order they’re written, i.e. from top to bottom.

User extensibility

Userland objects should be able to encapsulate their own matching semantics, without unnecessarily privileging builtins. This includes regular expressions (as opposed to the literal pattern syntax), numeric ranges, etc.

Prior Art

This proposal adds a pattern matching expression to the language, based in part on the existing Destructuring Binding Patterns.

This proposal was approved for Stage 1 in the May 2018 TC39 meeting, and slides for that presentation are available here. Its current form was presented to TC39 in the April 2021 meeting (slides).

This proposal draws from, and partially overlaps with, corresponding features in CoffeeScript, Rust, Python, F#, Scala, Elixir/Erlang, and C++.

Userland matching

A list of community libraries that provide similar matching functionality:

  • Optionals — Rust-like error handling, options and exhaustive pattern matching for TypeScript and Deno
  • ts-pattern — Exhaustive Pattern Matching library for TypeScript, with smart type inference.
  • babel-plugin-proposal-pattern-matching — Minimal grammar, high performance JavaScript pattern matching implementation.
  • match-iz — A tiny functional pattern-matching library inspired by the TC39 proposal.
  • patcom — Feature parity with TC39 proposal without any new syntax

Code samples

General terminology

match (res) {
  when ({ status: 200, body, ...rest }): handleData(body, rest)
  when ({ status, destination: url }) if (300 <= status && status < 400):
    handleRedirect(url)
  when ({ status: 500 }) if (!this.hasRetried): do {
    retry(req);
    this.hasRetried = true;
  }
  default: throwSomething();
}

match expression

  • The whole block beginning with the match keyword, is the match construct.

  • res is the matchable. This can be any expression.

  • There are four clauses in this example: three when clauses, and one default clause.

  • A clause consists of a left-hand side (LHS) and a right-hand side (RHS), separated by a colon (:).

  • The LHS can begin with the when or default keywords.

    • The when keyword must be followed by a pattern in parentheses. Each of the when clauses here contain object patterns.
    • The parenthesized pattern may be followed by a guard, which consists of the if keyword, and a condition (any expression) in parentheses. Guards provide a space for additional logic when patterns aren’t expressive enough.
    • An explicit default clause handles the "no match" scenario by always matching. It must always appear last when present, as any clauses after an default are unreachable.
  • The RHS is any expression. It will be evaluated if the LHS successfully matches, and the result will be the value of the entire match construct.

    • We assume that do expressions will mature soon, which will allow users to put multiple statements in an RHS; today, that requires an IIFE.

is expression

const problematic = res is { status: 500 };
if (problematic) logger.report(res);

More on combinators

match (command) {
  when ([ 'go', dir and ('north' or 'east' or 'south' or 'west')]): go(dir);
  when ([ 'take', item and /[a-z]+ ball/ and { weight }]): take(item);
  default: lookAround()
}

This sample is a contrived parser for a text-based adventure game.

The first clause matches if the command is an array with exactly two items. The first must be exactly the string 'go', and the second must be one of the given cardinal directions. Note the use of the and combinator to bind the second item in the array to dir using an identifier pattern before verifying (using the or combinator) that it’s one of the given directions.

(Note that there is intentionally no precedence relationship between the pattern operators, such as and, or, or with; parentheses must be used to group patterns using different operators at the same level.)

The second clause showcases a more complex use of the and combinator. First is an identifier pattern that binds the second item in the array to item. Then, there’s a regex pattern that checks if the item is a "something ball". Last is an object pattern, which checks that the item has a weight property (which, combined with the previous pattern, means that the item must be an exotic string object), and makes that binding available to the RHS.

Array length checking

match (res) {
  if (isEmpty(res)): ...
  when ({ data: [page] }): ...
  when ({ data: [frontPage, ...pages] }): ...
  default: { ... }
}

Array patterns implicitly check the length of the incoming matchable.

The first clause is a bare guard, which matches if the condition is truthy.

The second clause is an object pattern which contains an array pattern, which matches if data has exactly one element, and binds that element to page for the RHS.

The third clause matches if data has at least one element, binding that first element to frontPage, and binding an array of any remaining elements to pages using a rest pattern.

(Rest patterns can also be used in objects, with the expected semantics.)

Bindings from regex patterns with named capture groups

match (arithmeticStr) {
  when (/(?<left>\d+) \+ (?<right>\d+)/): process(left, right);
  when (/(\d+) \* (\d+)/ with [, left, right]): process(left, right);
  default: ...
}

This sample is a contrived arithmetic expression parser which uses regex patterns.

The first clause matches integer addition expressions, using named capture groups for each of the operands. The RHS is able to see the named capture groups as bindings.

(These magic bindings will only work with literal regex patterns. If a regex with named capture groups is passed into an interpolation pattern, the RHS will see no magic bindings. It’s very important (e.g. for code analysis tools) that bindings only be introduced where the name is locally present.)

The second clause matches integer multiplication expressions, but without named capture groups. Regexes (both literals and references inside interpolation patterns) implement the custom matcher protocol, which makes the return value of String.prototype.match available to the with operator.

(Regexes are a major motivator for the custom matcher protocol ― while we could treat them as a special case, they’re just ordinary objects. If they can be used as a pattern, then userland objects should be able to do this as well.)

Speaking of interpolations...

const LF = 0x0a;
const CR = 0x0d;

match (nextChar()) {
  when (${LF}): ...
  when (${CR}): ...
  default: ...
}

Here we see the interpolation operator (${}), which escapes from "pattern mode" syntax to "expression mode" syntax. It is conceptually very similar to using ${} in template strings.

Written as just LF, LF is an identifier pattern, which would always match regardless of the value of the matchable (nextChar()) and bind it to the given name (LF), shadowing the outer const LF = 0x0a declaration at the top.

Written as ${LF}, LF is evaluated as an expression, which results in the primitive Number value 0x0a. This value is then treated as a literal Number pattern, and the clause matches if the matchable is 0x0a. The RHS sees no new bindings.

Custom matcher protocol interpolations

class Option {
  #value;
  #hasValue = false;

  constructor (hasValue, value) {
    this.#hasValue = !!hasValue;
    if (hasValue) {
      this.#value = value;
    }
  }

  get value() {
    if (this.#hasValue) return this.#value;
    throw new Exception('Can’t get the value of an Option.None.');
  }

  static Some(val) {
    return new Option(true, val);
  }

  static None() {
    return new Option(false);
  }

  static {
    Option.Some[Symbol.matcher] = (val) => ({
      matched: #hasValue in val && val.#hasValue,
      value: #value in val && val.#value,
    });

    Option.None[Symbol.matcher] = (val) => ({
      matched: #hasValue in val && !val.#hasValue
    });
  }
}

match (result) {
  when (${Option.Some} with val): console.log(val);
  when (${Option.None}): console.log("none");
}

In this sample implementation of the common "Option" type, the expressions inside ${} are the static "constructors" Option.Some and Option.None, which have a Symbol.matcher method. That method is invoked with the matchable (result) as its sole argument. The interpolation pattern is considered to have matched if the Symbol.matcher method returns an object with a truthy matched property. Any other return value (including true by itself) indicates a failed match. (A thrown error percolates up the expression tree, as usual.)

The interpolation pattern can optionally chain into another pattern using with chaining, which matches against the value property of the object returned by the Symbol.matcher method; in this case, it allows Option.Some to expose the value inside of the Option.

Dynamic custom matchers can readily be created, opening a world of possibilities:

function asciiCI(str) {
  return {
    [Symbol.matcher](matchable) {
      return {
        matched: str.toLowerCase() == matchable.toLowerCase()
      };
    }
  }
}

match (cssProperty) {
  when ({ name: name and ${asciiCI("color")}, value }):
    console.log("color: " + value);
    // matches if `name` is an ASCII case-insensitive match
    // for "color", so `{name:"COLOR", value:"red"} would match.
}

Built-in custom matchers

match (value) {
  when (${Number}): ...
  when (${BigInt}): ...
  when (${String}): ...
  when (${Array}): ...
  default: ...
}

All the built-in classes come with a predefined Symbol.matcher method which uses brand check semantics to determine if the incoming matchable is of that type. If so, the matchable is returned under the value key.

Brand checks allow for predictable results across realms. So, for example, arrays from other windows will still successfully match the ${Array} pattern, similar to Array.isArray().

Motivating examples

Below are selected situations where we expect pattern matching will be widely used. As such, we want to optimize the ergonomics of such cases to the best of our ability.

Matching fetch() responses:

const res = await fetch(jsonService)
match (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): do {
    throw new RequestError(res);
  }
};

More concise, more functional handling of Redux reducers (compare with this same example in the Redux documentation):

function todosReducer(state = initialState, action) {
  return match (action) {
    when ({ type: 'set-visibility-filter', payload: visFilter }):
      { ...state, visFilter }
    when ({ type: 'add-todo', payload: text }):
      { ...state, todos: [...state.todos, { text, completed: false }] }
    when ({ type: 'toggle-todo', payload: index }): do {
      const newTodos = state.todos.map((todo, i) => {
        return i !== index ? todo : {
          ...todo,
          completed: !todo.completed
        };
      });

      ({
        ...state,
        todos: newTodos,
      });
    }
    default: state // ignore unknown actions
  }
}

Concise conditional logic in JSX (via Divjot Singh):

<Fetch url={API_URL}>
  {props => match (props) {
    when ({ loading }): <Loading />
    when ({ error }): do {
      console.err("something bad happened");
      <Error error={error} />
    }
    when ({ data }): <Page data={data} />
  }}
</Fetch>

Proposal

Match construct

Refers to the entire match (...) { ... } expression. Evaluates to the RHS of the first clause to match, or throws a TypeError if none match.

Matchable

The value a pattern is matched against. The top-level matchable shows up in match (matchable) { ... }, and is used for each clause as the initial matchable.

Destructuring patterns can pull values out of a matchable, using these sub-values as matchables for their own nested patterns. For example, matching against ["foo"] will confirm the matchable itself is an array-like with one item, then treat the first item as a matchable against the "foo" primitive pattern.

Clause

One "arm" of the match construct’s contents, consisting of an LHS (left-hand side) and an RHS (right-hand side), separated by a colon (:).

The LHS can look like:

  • when (<pattern>), which matches its pattern against the top-level matchable;
  • if (<expr>), which matches if the <expr> is truthy;
  • when (<pattern>) if (<expr>), which does both;
  • default, which always succeeds but must be the final clause.

The RHS is an arbitrary JS expression, which the whole match construct resolves to if the LHS successfully matches.

(There is an open issue about whether there should be some separator syntax between the LHS and RHS.)

The LHS’s patterns, if any, can introduce variable bindings which are visible to the guard and the RHS of the same clause. Bindings are not visible across clauses. Each pattern describes what bindings, if any, it introduces.

TODO: LHS

TODO: RHS

Guard

The if (<expr>) part of a clause. The <expr> sees bindings present at the start of the match construct; if the clause began with a when (<pattern>), it additionally sees the bindings introduced by the pattern.

Pattern

There are several types of patterns:

Primitive Pattern

Boolean literals, numeric literals, string literals, and the null literal.

Additionally, some expressions that are almost literals, and function as literals in people’s heads, are allowed:

  • undefined, matching the undefined value
  • numeric literals preceded by an unary + or -, like -1
  • NaN
  • Infinity (with + or - prefixes as well)
  • untagged template literals, with the interpolation expressions seeing only the bindings present at the start of the match construct.

These match if the matchable is SameValue with them, with one exception: if the pattern is the literal 0 (without the unary prefix operators +0 or -0), it is instead compared with SameValueZero.

(That is, +0 and -0 only match positive and negative zero, respectively, while 0 matches both zeroes without regard for the sign.)

They do not introduce bindings.

Identifier Pattern

Any identifier that isn’t a primitive matcher, such as foo. These always match, and bind the matchable to the given binding name.

Regex Pattern

A regular expression literal.

The matchable is stringified, and the pattern matches if the string matches the regex. If the regex defines named capture groups, those names are introduced as bindings, bound to the captured substrings. Regex patterns can use with-chaining to further match a pattern against the regex’s match result.

Interpolation pattern

An arbitrary JS expression wrapped in ${}, just like in template literals. For example, ${myVariable}, ${"foo-" + restOfString}, or ${getValue()}.

At runtime, the expression inside the ${} is evaluated. If it resolves to an object with a method named Symbol.matcher, that method is invoked, and matching proceeds with the custom matcher protocol semantics. If it resolves to anything else (typically a primitive, a Symbol, or an object without a Symbol.matcher function), then the pattern matches if the matchable is SameValue with the result.

Interpolation patterns can use with-chaining to further match against the value key of the object returned by the Symbol.matcher method.

Array Pattern

A comma-separated list of zero or more patterns or holes, wrapped in square brackets, like ["foo", a, {bar}]. "Holes" are just nothing (or whitespace), like [,,thirdItem]. The final item can optionally be either a "rest pattern", looking like ..., or a "binding rest pattern", looking like ...<identifier>. (Aka, an array pattern looks like array destructuring, save for the addition of the "rest pattern" variant.)

First, an iterator is obtained from the matchable: if the matchable is itself iterable (exposes a [Symbol.iterator] method) that is used; if it’s array-like, an array iterator is used.

Then, items are pulled from the iterator, and matched against the array pattern’s corresponding nested patterns. (Holes always match, introducing no bindings.) If any of these matches fail, the entire array pattern fails to match.

If the array pattern ends in a binding rest pattern, the remainder of the iterator is pulled into an Array, and bound to the identifier from the binding rest pattern, just like in array destructuring.

If the array pattern does not end in a rest pattern (binding or otherwise), the iterator must match the array pattern’s length: one final item is pulled from the iterator, and if it succeeds (rather than closing the iterator), the array pattern fails to match.

The array pattern introduces all the bindings introduced by its nested patterns, plus the binding introduced by its binding rest pattern, if present.

Bindings introduced by earlier nested patterns are visible to later nested patterns in the same array pattern. (For example, [a, ${a}]) will match only if the second item in the array is identical to the first item.)

Array Pattern Caching

To allow for idiomatic uses of generators and other "single-shot" iterators to be reasonably matched against several array patterns, the iterators and their results are cached over the scope of the match construct.

Specifically, whenever a matchable is matched against an array pattern, the matchable is used as the key in a cache, whose value is the iterator obtained from the matchable, and all items pulled from the matchable by an array pattern.

Whenever something would be matched against an array pattern, the cache is first checked, and the already-pulled items stored in the cache are used for the pattern, with new items pulled from the iterator only if necessary.

For example:

function* integers(to) {
  for(var i = 1; i <= to; i++) yield i;
}

const fiveIntegers = integers(5);
match (fiveIntegers) {
  when([a]):
    console.log(`found one int: ${a}`);
    // Matching a generator against an array pattern.
    // Obtain the iterator (which is just the generator itself),
    // then pull two items:
    // one to match against the `a` pattern (which succeeds),
    // the second to verify the iterator only has one item
    // (which fails).
  when([a, b]):
    console.log(`found two ints: ${a} and ${b}`);
    // Matching against an array pattern again.
    // The generator object has already been cached,
    // so we fetch the cached results.
    // We need three items in total;
    // two to check against the patterns,
    // and the third to verify the iterator has only two items.
    // Two are already in the cache,
    // so we’ll just pull one more (and fail the pattern).
  default: console.log("more than two ints");
}
console.log([...fiveIntegers]);
// logs [4, 5]
// The match construct pulled three elements from the generator,
// so there’s two leftover afterwards.

When execution of the match construct finishes, all cached iterators are closed.

Object Pattern

A comma-separated list of zero or more "object pattern clauses", wrapped in curly braces, like {x: "foo", y, z: {bar}}. Each "object pattern clause" is either an <identifier>, or a <key>: <pattern> pair, where <key> is an <identifier> or a computed-key expression like [Symbol.foo]. The final item can be a "rest pattern", looking like ...<identifier>. (Aka, it looks like object destructuring.)

For each object pattern clause, the matchable must contain a property matching the key, and the value of that property must match the corresponding pattern; if either of these fail for any object pattern clause, the entire object pattern fails to match.

Plain <identifier> object pattern clauses are treated as if they were written <identifier>: <identifier> (just like destructuring); that is, the matchable must have the named property, and the property’s value is then bound to that name due to being matched against an identifier pattern.

If the object pattern ends in a [TODO: rest pattern], all of the matchable’s own keys that weren’t explicitly matched are bound into a fresh Object, just like destructuring or array patterns.

Unlike array patterns, the lack of a final rest pattern imposes no additional constraints; {foo} will match the object {foo: 1, bar:2}, binding foo to 1 and ignoring the other key.

The object pattern introduces all the bindings introduced by its nested patterns, plus the binding introduced by its rest pattern, if present.

Bindings introduced by earlier nested patterns are visible to later nested patterns in the same object pattern. (For example, {a, b:${a}}) will match only if the b property item in the object is identical to the a property's value.) Ordering is important, however, so {b:${a}, a} does not mean the same thing; instead, the ${a} resolves based on whatever a binding might exist from earlier in the pattern, or outside the match construct entirely.

Object Pattern Caching

Similar to array pattern caching, object patterns cache their results over the scope of the match construct, so that multiple clauses don’t observably retrieve the same property multiple times.

(Unlike array pattern caching, which is necessary for this proposal to work with iterators, object pattern caching is a nice-to-have. It does guard against some weirdness like non-idempotent getters, and helps make idempotent-but-expensive getters usable in pattern matching without contortions, but mostly it’s just for conceptual consistency.)

Whenever a matchable is matched against an object pattern, for each property name in the object pattern, a (<matchable>, <property name>) tuple is used as the key in a cache, whose value is the value of the property.

Whenever something would be matched against an object pattern, the cache is first checked, and if the matchable and that property name are already in the cache, the value is retrieved from cache instead of by a fresh Get against the matchable.

For example:

const randomItem = {
  get numOrString() { return Math.random() < .5 ? 1 : "1"; }
};

match (randomItem) {
  when({numOrString: ${Number}}):
    console.log("Only matches half the time.");
    // Whether the pattern matches or not,
    // we cache the (randomItem, "numOrString") pair
    // with the result.
  when({numOrString: ${String}}):
    console.log("Guaranteed to match the other half of the time.");
    // Since (randomItem, "numOrString") has already been cached,
    // we reuse the result here;
    // if it was a string for the first clause,
    // it’s the same string here.
}

TODO: Rest pattern

Custom Matcher Protocol

When the expression inside an interpolation pattern evaluates to an object with a Symbol.matcher method, that method is called with the matchable as its sole argument.

To implement the Symbol.matcher method, the developer must return an object with a matched property. If that property is truthy, the pattern matches; if that value is falsy, the pattern does not match. In the case of a successful match, the matched value must be made available on a value property of the return object.

Built-in Custom Matchers

All of the classes for primitive types (Boolean, String, Number, BigInt) expose a built-in Symbol.matcher method, matching if and only if the matchable is an object of that type, or a primitive corresponding to that type (using brand-checking to check objects, so boxed values from other windows will still match). The value property of the returned object is the (possibly auto-unboxed) primitive value.

All other platform objects also expose built-in Symbol.matcher methods, matching if and only if the matchable is of the same type (again using brand-checking to verify, similar to Array.isArray()). The value property of the returned object is the matchable itself.

Userland classes do not define a default custom matcher (for both practical and technical reasons), but it is very simple to define one in this style:

class Foo {
  static [Symbol.matcher](value) {
    return {
      matched: value instanceof Foo,
      value,
    };
  }
}

with chaining

An interpolation pattern or a regex pattern (referred to as the "parent pattern" for the rest of this section) may also have a with <pattern> suffix, allowing you to provide further patterns to match against the parent pattern’s result.

The with pattern is only invoked if the parent pattern successfully matches. Any bindings introduced by the with pattern are added to the bindings from the parent pattern, with the with pattern’s values overriding the parent pattern’s value if the same bindings appear in both.

The parent pattern defines what the matchable will be for the with pattern:

  • for regex patterns, the regex’s match object is used
  • for interpolation patterns that did not invoke the custom matcher protocol, the matchable itself is used
  • for interpolation patterns that did invoke the custom matcher protocol, the value of the value property on the result object is used

For example:

class MyClass = {
  static [Symbol.matcher](matchable) {
    return {
      matched: matchable === 3,
      value: { a: 1, b: { c: 2 } },
    };
  }
};

match (3) {
  when (${MyClass}): true; // matches, doesn’t use the result
  when (${MyClass} with {a, b: {c}}): do {
    // passes the custom matcher,
    // then further applies an object pattern to the result’s value
    assert(a === 1);
    assert(c === 2);
  }
}

or

match ("foobar") {
  when (/foo(.*)/ with [, suffix]):
    console.log(suffix);
    // logs "bar", since the match result
    // is an array-like containing the whole match
    // followed by the groups.
    // note the hole at the start of the array matcher
    // ignoring the first item,
    // which is the entire match "foobar".
}

Pattern combinators

Two or more patterns can be combined with or or and to form a single larger pattern.

A sequence of or-separated patterns have short-circuiting "or" semantics: the or pattern matches if any of the nested patterns match, and stops executing as soon as one of its nested patterns matches. It introduces all the bindings introduced by its nested patterns, but only the values from its first successfully matched pattern; bindings introduced by other patterns (either failed matches, or patterns past the first successful match) are bound to undefined.

A sequence of and-separated patterns have short-circuiting "and" semantics: the and pattern matches if all of the nested patterns match, and stops executing as soon as one of its nested patterns fails to match. It introduces all the bindings introduced by its nested patterns, with later patterns providing the value for a given binding if multiple patterns would introduce that binding.

Note that and can idiomatically be used to bind a matchable and still allow it to be further matched against additional patterns. For examle, when (foo and [bar, baz]) ... matches the matchable against both the foo identifier pattern (binding it to foo for the RHS) and against the [bar, baz] array pattern.

Bindings introduced by earlier nested patterns are visible to later nested patterns in the same combined pattern. (For example, (a and ${console.log(a)||a})) will bind the matchable to a, and then log it.)

(Note: the and and or spellings of these operators are preferred by the champions group, but we'd be okay with spelling them & and | if the committee prefers.

Parenthesizing Patterns

The pattern syntaxes do not have a precedence relationship with each other. Any multi-token patterns (and, or, ${...} with ...) appearing at the same "nesting level" are a syntax error; parentheses must be used to to specify their relationship to each other instead.

For example, when ("foo" or "bar" and val) ... is a syntax error; it must be written as when ("foo" or ("bar" and val)) ... or when (("foo" or "bar") and val) instead. Similarly, when (${Foo} with bar and baz) ... is a syntax error; it must be written as when (${Foo} with (bar and baz)) ... (binding the custom match result to both bar and baz) or when ((${Foo} with bar) and baz) ... (binding the custom match result to bar, and the original matchable to baz).

is expression

Refers to the expr is pattern expression. Evaluates to a boolean to indicate if the LHS matches the RHS.

Possible future enhancements

async match

If the match construct appears inside a context where await is allowed, await can already be used inside it, just like inside do expressions. However, just like async do expressions, there’s uses of being able to use await and produce a Promise, even when not already inside an async function.

async match (await matchable) {
  when ({ a }): await a;
  when ({ b }): b.then(() => 42);
  default: await somethingThatRejects();
} // produces a Promise

Nil pattern

match (someArr) {
  when ([_, _, someVal]): ...
}

Most languages that have structural pattern matching have the concept of a "nil matcher", which fills a hole in a data structure without creating a binding.

In JS, the primary use-case would be skipping spaces in arrays. This is already covered in destructuring by simply omitting an identifier of any kind in between the commas.

With that in mind, and also with the extremely contentious nature, we would only pursue this if we saw strong support for it.

Default Values

Destructuring can supply a default value with = <expr> which is used when a key isn’t present. Is this useful for pattern matching?

Optional keys seem reasonable; right now they’d require duplicating the pattern like ({a, b} or {a}) (b will be bound to undefined in the RHS if not present).

Do we need/want full defaulting? Does it complicate the syntax to much to have arbitrary JS expressions there, without anything like wrapper characters to distinguish it from surrounding patterns?

This would bring us into closer alignment with destructuring, which is nice.

Dedicated renaming syntax

Right now, to bind a value in the middle of a pattern but continue to match on it, you use and to run both an identifier pattern and a further pattern on the same value, like when(arr and [item]): ....

Langs like Haskell and Rust have a dedicated syntax for this, spelled @; if we adopted this, the above could be written as when(arr @ [item]): ....

Since this would introduce no new functionality, just a dedicated syntactic form for a common operation and some amount of concordance with other languages, we’re not pursuing this as part of the base proposal.

Destructuring enhancements

Both destructuring and pattern matching should remain in sync, so enhancements to one would need to work for the other.

Integration with catch

Allow a catch statement to conditionally catch an exception, saving a level of indentation:

try {
  throw new TypeError('a');
} catch match (e) {
  if (e instanceof RangeError): ...
  when (/^abc$/): ...
  default: do { throw e; } // default behavior
}

Chaining guards

Some reasonable use-cases require repetition of patterns today, like:

match (res) {
  when ({ pages, data }) if (pages > 1): console.log("multiple pages")
  when ({ pages, data }) if (pages === 1): console.log("one page")
  default: console.log("no pages")
}

We might want to allow match constructs to be chained, where the child match construct sees the bindings introduced in their parent clause, and which will cause the entire parent clause to fail if none of the sub-classes match.

The above would then be written as:

match (res) {
  when ({ pages, data }) match {
    if (pages > 1): console.log("multiple pages")
    if (pages === 1): console.log("one page")
    // if pages == 0, no clauses succeed in the child match,
    // so the parent clause fails as well,
    // and we advance to the outer `default`
  }
  default: console.log("no pages")
}

Note the lack of matchable in the child (just match {...}), to signify that it’s chaining from the when rather than just being part an independent match construct in the RHS (which would, instead, throw if none of the clauses match):

match (res) {
  when ({ pages, data }): match (0) {
    if(pages > 1): console.log("multiple pages")
    if(pages === 1): console.log("one page")
    // just an RHS, so if pages == 0,
    // the inner construct fails to match anything
    // and throws a TypeError
  }
  default: console.log("no pages")
}

The presence or absence of the separator colon also distinguishes these cases, of course.

or on when clauses

There might be some cases that requires different when + if guards with the same RHS.

// current
match (expr()) {
    when ({ type: 'a', version, ...rest }) if (isAcceptableTypeVersion(version)):
        a_long_expression_do_something_with_rest
    when ({ kind: 'a', version, ...rest }) if (isAcceptableKindVersion(version)):
        a_long_expression_do_something_with_rest
}

Today this case can be resolved by extracting a_long_expression_do_something_with_rest to a function, but if cases above are very common, we may also allows or to be used on the when clause, and the code above becomes:

// current
match (expr()) {
    when ({ type: 'a', version, ...rest }) if (isAcceptableTypeVersion(version))
    or when ({ kind: 'a', version, ...rest }) if (isAcceptableKindVersion(version)):
        a_long_expression_do_something_with_rest
}

More Repositories

1

proposals

Tracking ECMAScript Proposals
17,177
star
2

ecma262

Status, process, and documents for ECMA-262
HTML
14,437
star
3

proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
HTML
7,380
star
4

proposal-optional-chaining

HTML
4,952
star
5

proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
JavaScript
4,090
star
6

proposal-temporal

Provides standard objects and functions for working with dates and times.
HTML
3,135
star
7

proposal-observable

Observables for ECMAScript
JavaScript
3,032
star
8

proposal-signals

A proposal to add signals to JavaScript.
TypeScript
2,668
star
9

proposal-decorators

Decorators for ES6 classes
2,640
star
10

proposal-record-tuple

ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!
HTML
2,423
star
11

test262

Official ECMAScript Conformance Test Suite
JavaScript
2,073
star
12

proposal-dynamic-import

import() proposal for JavaScript
HTML
1,859
star
13

proposal-bind-operator

This-Binding Syntax for ECMAScript
1,736
star
14

proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
HTML
1,720
star
15

proposal-async-await

Async/await for ECMAScript
HTML
1,577
star
16

proposal-object-rest-spread

Rest/Spread Properties for ECMAScript
HTML
1,496
star
17

proposal-shadowrealm

ECMAScript Proposal, specs, and reference implementation for Realms
HTML
1,365
star
18

proposal-nullish-coalescing

Nullish coalescing proposal x ?? y
HTML
1,233
star
19

proposal-iterator-helpers

Methods for working with iterators in ECMAScript
HTML
1,220
star
20

proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 4)
HTML
1,082
star
21

proposal-partial-application

Proposal to add partial application to ECMAScript
HTML
1,002
star
22

proposal-do-expressions

Proposal for `do` expressions
HTML
990
star
23

agendas

TC39 meeting agendas
JavaScript
952
star
24

proposal-binary-ast

Binary AST proposal for ECMAScript
945
star
25

proposal-built-in-modules

HTML
886
star
26

proposal-async-iteration

Asynchronous iteration for JavaScript
HTML
854
star
27

proposal-explicit-resource-management

ECMAScript Explicit Resource Management
JavaScript
671
star
28

proposal-operator-overloading

JavaScript
610
star
29

proposal-string-dedent

TC39 Proposal to remove common leading indentation from multiline template strings
HTML
588
star
30

proposal-bigint

Arbitrary precision integers in JavaScript
HTML
560
star
31

proposal-set-methods

Proposal for new Set methods in JS
HTML
557
star
32

proposal-import-attributes

Proposal for syntax to import ES modules with assertions
HTML
538
star
33

ecmascript_simd

SIMD numeric type for EcmaScript
JavaScript
536
star
34

proposal-slice-notation

HTML
515
star
35

proposal-change-array-by-copy

Provides additional methods on Array.prototype and TypedArray.prototype to enable changes on the array by returning a new copy of it with the change.
HTML
509
star
36

ecma402

Status, process, and documents for ECMA 402
HTML
506
star
37

notes

TC39 meeting notes
JavaScript
496
star
38

proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
HTML
489
star
39

proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()
HTML
464
star
40

proposal-uuid

UUID proposal for ECMAScript (Stage 1)
JavaScript
462
star
41

proposal-throw-expressions

Proposal for ECMAScript 'throw' expressions
JavaScript
425
star
42

proposal-module-expressions

HTML
424
star
43

proposal-UnambiguousJavaScriptGrammar

413
star
44

proposal-decimal

Built-in decimal datatype in JavaScript
HTML
408
star
45

proposal-array-grouping

A proposal to make grouping of array items easier
HTML
407
star
46

proposal-async-context

Async Context for JavaScript
HTML
406
star
47

proposal-weakrefs

WeakRefs
HTML
404
star
48

proposal-error-cause

TC39 proposal for accumulating errors
HTML
378
star
49

proposal-ecmascript-sharedmem

Shared memory and atomics for ECMAscript
HTML
376
star
50

proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Shell
376
star
51

proposal-relative-indexing-method

A TC39 proposal to add an .at() method to all the basic indexable classes (Array, String, TypedArray)
HTML
351
star
52

proposal-first-class-protocols

a proposal to bring protocol-based interfaces to ECMAScript users
350
star
53

proposal-global

ECMAScript Proposal, specs, and reference implementation for `global`
HTML
346
star
54

proposal-private-methods

Private methods and getter/setters for ES6 classes
HTML
344
star
55

proposal-numeric-separator

A proposal to add numeric literal separators in JavaScript.
HTML
327
star
56

proposal-private-fields

A Private Fields Proposal for ECMAScript
HTML
320
star
57

proposal-object-from-entries

TC39 proposal for Object.fromEntries
HTML
317
star
58

proposal-module-declarations

JavaScript Module Declarations
HTML
314
star
59

proposal-promise-allSettled

ECMAScript Proposal, specs, and reference implementation for Promise.allSettled
HTML
314
star
60

tc39.github.io

Get involved in specifying JavaScript
HTML
313
star
61

proposal-regex-escaping

Proposal for investigating RegExp escaping for the ECMAScript standard
JavaScript
309
star
62

proposal-await.ops

Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises
HTML
308
star
63

proposal-logical-assignment

A proposal to combine Logical Operators and Assignment Expressions
HTML
302
star
64

proposal-export-default-from

Proposal to add `export v from "mod";` to ECMAScript.
HTML
297
star
65

proposal-promise-finally

ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
HTML
278
star
66

proposal-asset-references

Proposal to ECMAScript to add first-class location references relative to a module
268
star
67

proposal-cancellation

Proposal for a Cancellation API for ECMAScript
HTML
262
star
68

proposal-json-modules

Proposal to import JSON files as modules
HTML
259
star
69

proposal-promise-with-resolvers

HTML
255
star
70

proposal-string-replaceall

ECMAScript proposal: String.prototype.replaceAll
HTML
254
star
71

proposal-export-ns-from

Proposal to add `export * as ns from "mod";` to ECMAScript.
HTML
241
star
72

proposal-ses

Draft proposal for SES (Secure EcmaScript)
HTML
217
star
73

proposal-structs

JavaScript Structs: Fixed Layout Objects
216
star
74

proposal-intl-relative-time

`Intl.RelativeTimeFormat` specification [draft]
HTML
215
star
75

proposal-flatMap

proposal for flatten and flatMap on arrays
HTML
215
star
76

proposal-json-parse-with-source

Proposal for extending JSON.parse to expose input source text.
HTML
204
star
77

ecmarkup

An HTML superset/Markdown subset source format for ECMAScript and related specifications
TypeScript
201
star
78

proposal-promise-any

ECMAScript proposal: Promise.any
HTML
198
star
79

proposal-decorators-previous

Decorators for ECMAScript
HTML
184
star
80

proposal-smart-pipelines

Old archived draft proposal for smart pipelines. Go to the new Hack-pipes proposal at js-choi/proposal-hack-pipes.
HTML
181
star
81

proposal-defer-import-eval

A proposal for introducing a way to defer evaluate of a module
HTML
174
star
82

proposal-array-filtering

A proposal to make filtering arrays easier
HTML
171
star
83

proposal-optional-chaining-assignment

`a?.b = c` proposal
168
star
84

proposal-array-from-async

Draft specification for a proposed Array.fromAsync method in JavaScript.
HTML
167
star
85

proposal-extractors

Extractors for ECMAScript
JavaScript
166
star
86

proposal-upsert

ECMAScript Proposal, specs, and reference implementation for Map.prototype.upsert
HTML
165
star
87

proposal-ptc-syntax

Discussion and specification for an explicit syntactic opt-in for Tail Calls.
HTML
165
star
88

how-we-work

Documentation of how TC39 operates and how to participate
161
star
89

proposal-collection-methods

HTML
160
star
90

proposal-Array.prototype.includes

Spec, tests, reference implementation, and docs for ESnext-track Array.prototype.includes
HTML
157
star
91

proposal-error-stacks

ECMAScript Proposal, specs, and reference implementation for Error.prototype.stack / System.getStack
HTML
156
star
92

proposal-promise-try

ECMAScript Proposal, specs, and reference implementation for Promise.try
HTML
154
star
93

proposal-hashbang

#! for JS
HTML
148
star
94

proposal-resizablearraybuffer

Proposal for resizable array buffers
HTML
145
star
95

proposal-import-meta

import.meta proposal for JavaScript
HTML
145
star
96

proposal-intl-segmenter

Unicode text segmentation for ECMAScript
HTML
145
star
97

proposal-extensions

Extensions proposal for ECMAScript
HTML
143
star
98

proposal-seeded-random

Proposal for an options argument to be added to JS's Math.random() function, and some options to start it with.
HTML
143
star
99

proposal-intl-duration-format

141
star
100

proposal-regexp-unicode-property-escapes

Proposal to add Unicode property escapes `\p{…}` and `\P{…}` to regular expressions in ECMAScript.
HTML
134
star