Pattern matching is a tough nut to crack, specially for a dynamic language and particularly for ECMAScript. I feel the current state of the proposal opens too many fronts with quite a few novelties and ambiguities. Some of the complexity has to do with testing for values in addition to destructuring patterns.
So I propose matching patterns with _values_ be __removed__ from this proposal entirely.
Equality and ambiguity: I know matching on values is tempting, even desirable, but ES has such a dense background regarding equality that doing it implicitly does not seem like a good idea. Conditionals in ES code usually require mindful checks. A simple when { status: 200 } potentially seeds doubt onto the minds of beginners and seasoned developers alike: is it === or ==? The current proposal introduces when and if to alleviate that, but with the price of ambiguity and novel syntax.
Too many {k:v} meanings: proposed syntax would be the third meaning of { literal: X } in the language (if we exclude TypeScript type annotations): { literal: value } (initializer), { literal: alias }, { literal: match }. Not to mention {x}={x:10}. This is all added cognitive overload.
Value-returning statement: the proposal introduces (or reassigns) 3 keywords, case, when, if and the ->. And it does it inline, becoming one of few block-holding keywords that support an lvalue (besides function() IIRC). Also TC39 proposed a do keyword that could later enable this globally (ie. do case() { ... }). Is it really needed now?
Nested return: the case() block returns an implicit value (from the dispatch) without a return and from 2 levels of indentation deep, potentially more as they could be chained. This is unprecedented in ES.
Scoped context value: the syntax depends on an invisible contextual value in the case() that will be compared implicitly against the many when instances. This seems ok at first, as the switch statement does the same, but we don't like switch, do we? The same applies to with(), another language outcast. It's probably just a coincidence that both "outcasts" depend on context scopes, or isn't it?
I know, getting rid of pattern value-matching is a big purge to the current proposal, but it's a good one for the sake of simplicity and effectiveness. As a counter-proposal, I have to 2 changes to the language that, Pareto style, could give the most benefits with least impact.
(My proposal is based on this 2018 Java proposal: https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html)
1) a right-side destructuring instanceof - in this case p instanceof { x, y } which checks the undefinedness of bound variables.
2) scoped conditional declarations - instanceof declares (as a debatable const) the bound variables into a local scope. These are variables that are local to the if(...) or ?:. This is a novelty in ES but it exists in Golang, Perl and Ruby to name a few. It is also similar to how for(const|let x ...) block scoped variables work.
Examples:
if( p instanceof { x, y, z }) {
// p is an object && p.x !== undefined && p.y !== undefined && p.z !== undefined
return Math.sqrt( x ** 2 + y ** 2 + z ** 2 );
}
else if( p instanceof { x, y }) {
// p is an object && p.x !== undefined && p.y !== undefined
return Math.sqrt( x ** 2 + y ** 2 );
}
else if( p instanceof [x, ...args]) {
// p is iterable && p[0] !== undefined
}
else if( p instanceof {x=1, y=1}) {
// always matches if p is an object
}
else if( p === undefined ) {
// etc.. more flexibility in dispatching
}
else {
console.log(x); // ReferenceError, x not defined
}
if( res instanceof { status } && status === 301 ) {
// after instanceof, status is bound to a local scope
console.log(status);
}
console.log(status); // ReferenceError, status not defined here either
if( res instanceof { error } || error === 'not found' ) {
// ReferenceError: error not defined because instanceof did not match the pattern
// ... or just have error be undefined to avoid some edge cases and confusion
}
const x = res instanceof { status } ?
status === 301 ? 'foo' :
status === 302 ? 'bar' :
'quz';
res instanceof { status }; // error, cannot destructure outside a conditional scope
if( !( p instanceof {x,y} ) {
// no x or y defined here
}
else {
console.log(x,y); // they do exist here
}
<Fetch url={API_URL}>{
props => (
props instanceof {loading} ? <Loading />
: props instanceof {error} ? <Error error={error} />
: props instanceof {data} ? <Page data={data} />
)
}
</Fetch>
// A redo of the redux example from the README:
function todoApp (state = initialState, action) {
return action instanceof {type, filter: visFilter} && type === 'set-visibility-filter'
? ({...state, visFilter}) :
action instanceof {type, text} && type === 'add-todo'
? ({...state, todos: [...state.todos, {text}]}) :
action instanceof {type, index} && type === 'toggle-todo'
? ({
...state,
todos: state.todos.map((todo, idx) => idx === index
? {...todo, done: !todo.done}
: todo
)
}) :
state // ignore unknown actions
}
Key takeaways:
x instanceof PATTERN will destructure x into PATTERN and declare a locally scoped var to the current block, either an if-else or ?: statement scopes. Only destructuring patterns can be used, namely {} and [].
instanceof will create inlined local scopes for && but not for ||. It means it needs to match a pattern first before the bound variables are initialized. The same applies to if-else.
PATTERN can have default values, aliases and all current destructuring syntax, as an operand (right side) of instanceof just like in const PATTERN = x.
matching objects is an AND on properties not being undefined. p instanceof {x,y} translates to p is an object && p.x !== undefined && p.y !== undefined. The check is safe and will not throw if p is not an object.
properties with default values and spread/rest variables are not tested for undefinedness. x instanceof [...args] is always true if x is iterable.
Testing many patterns requires repeating the object instanceof part several times, but it seems like a very small price to pay for clarity and a modular syntax as opposed to a contextual case().
Now this proposal doesn't do much for dispatching itself, it dispatches using existing syntax, but it would set a foundation for simple and effective _pattern matching_ and leave dispatching to be addressed separately by a new conditional on a different proposal. The Java proposal also has an interesting "upgrade" recommendation for switch-case where instead of : an arrow -> is introduced, that could give some ideas, maybe even fixing switch-case entirely for the language (but not here).
The 2 key gotchas I see with my proposal are:
1) it reinforces the use of instanceof, although avoiding the bad, cross-realm prototyping issues. If not instanceof we could consider a similar keyword that could be exclusively used for this, ie. x is PATTERN, x as PATTERN or x match PATTERN.
2) it introduces a local scope for the bound variables that is quite novel and would need some transpiling gymnastics to have it working in Babel. Local scoping can be analyzed deeper for edge cases also if needed. There are other ways of doing conditional scoping.
These issues should be relatively simple to deal with.
I hope this can help steer the proposal into a direction that can get pattern matching implemented as I'd really like to see it in the language.
instanceof is a nonstarter imo, because (as you noted) it doesn't work for builtins across realms. Also, 3 instanceof Number is false, and pattern matching needs to be able to match primitives.
I don't consider these issues simple to deal with at all.
3 instanceof Number is not part of this pattern matching proposal, which only deals with objects and iterators. This issue is simple to deal with if we rename instanceof to something else then.
It's not the name, it's the runtime semantics. There does not exist a generic cross-realm way to identify builtins, so this proposal shouldn't imply there is one.
This seems like the most confusing option of all, adding new syntax and changing meaning of existing syntax. Aren't all your concerns with the ultrasimple functional approach met if we say that only === is used for equality, no different than the existing switch or destructuring syntax? Not to mention a primary motive of pattern matching is more ergonomic syntax than predicates in if/else statements.
It introduces less syntax than the current proposal and doesn't step on so many language realms as having a new switch-case called case-when-if with a new arrow, context loading, returning statement and implicit equality.
That additional syntax imo makes it simpler than your proposal, because it's easier to understand that it's a different thing, and imo it's also very intuitive what it does and means.
Ergonomic syntax is desired, but could be addressed separately as a special dispatching syntax. Pattern matching is best if introduced as conditional destructuring.
If your goal is just conditional destructuring, seems like this rust-style solution would be much better without overloading instanceof:
if (let {a: [b, c]} = myObj) {
// do something with b, c
}
Yes, but not quite due to an inconsistency. I'd love to have const and let declare conditionally scoped variables, and I had that conversation before with tc39 in regards to introducing a returning let and const in the if.
The problem is that this actually already works:
if( { x, y } = { x: 10, y: 20 } ) console.log(x,y); // 10 20
But this would behave differently from pattern matching with let and const:
if( { x, y } = {} ) console.log(x,y); // prints undefined undefined
In this case { x, y } returns true as it's an object, a truey value. If let and const returned the result of the success of the pattern match it would make the 2 variations inconsistent.
Some history lessons:
JavaScript does not have nominal types (except for the primitive types) and runtime structural type checking is very expensive.

some more:

Ok please forget I mentioned instanceof, what I'm trying to get across translates to any keyword of your preference as my intention is to pattern-match using value KEYWORD PATTERN not to fix or regurgitate instanceof into the throat of JS programmers.
Take a look at C# as it has the is keyword that has a better parallel to what I'm proposing (I should have brought that up first), then separate constructs for dispatching / switching.
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/is
However I'm not a big fan of is as keyword because it's too similar to Object.is() and would look like a keyword that targets the SameValue algorithm.
if( p is { x, y } ) {
// p is an object && p.x !== undefined && p.y !== undefined
return x * y;
}
else if( p is [x, ...args] ) {
// x is iterator && p[0] !== undefined
}
else if( res is { status } && status === 400 ) {
// p is an object && p.x !== undefined && p.y !== undefined
return 'unauthorized';
}
Then dispatching, just like in C# and its switch { ... }, could be addressed at a later time, on a different proposal.
We can’t really add a keyword to represent semantics that aren’t currently possible in the language, I’d think.
Btw, I've just realized pattern-matching with instanceof made it into Java 14 a few weeks ago.
https://www.techgeeknext.com/java/java14-features#pmi
Like I said forget about instanceof, but the concept of a leaner, comparison operator stands.
C# actually did a great job implementing pattern matching in different phased introductions (C# 7.0 and 8.0 introduced different operators for pattern matching such as is and switch).
A more C#-oriented proposal a single keyword is would look like this:
if( p is {x,y} ) {
return x * y;
}
if( res is {status} && status !== 200 ) {
throw new Error(`response status is ${status}`);
};
const msg = p is [x,...args] ? x : 'nope';
Then add dispatching:
const x = p is {
{ x, y, z } -> x * y * z,
{ x, y } -> x * y
};
With guards:
const msg = res is {
( { status } && status === 200 ) -> 'ok',
( { status } && status !== 200 ) -> 'nok',
( { err } && err.length > 0 ) -> {
`error ${err}`;
},
() -> 'default'
};
Note the difference is the presence or absence of arrow dispatching within the is operator block:
// is returns boolean if no arrow dispatches are present
if( p is {x,y} ) ...; // true or false
if( p is { {x} -> x, {x,y} -> x * y } > 0 ) ...; // arrows dispatch a value instead of boolean
I like how it would support Typescript type annotations cleanly too, in a similar manner as they are used in functions:
// this implies that msg is type string | number
const msg = res is {
({ msg: string }) : string -> msg.toLowerCase(),
() : number -> 0
};
This, or some variation of it, imo would be a more idiomatic use of pattern-matching, keeping value guards separately as part of the predicate.
Destructuring is one of the great, most loved language features. This would expand on its superpowers.
If you can demonstrate how you'd do that comparison in the language right now, then it's worth discussing syntax for it - although that would likely need to be a separate proposal, that pattern matching would employ once it existed (but not before).
(specifically, because such an "is" check would need to work both for builtins cross-realm, and for user code, which means there needs to be a way for user code to opt in to it, and a robust way for first-run code to prevent builtins from being opted out of it, eg, a brand check)
This proposal:
const p = { z: 30 };
if( p is {x,y} ) {
console.log(`first p=${x + y}`);
}
else if( p is {z} ) {
console.log(`second p=${z}`);
}
else if( p is [x,y,...rest] ) {
console.log('array', x, y);
}
else {
console.log('default');
}
Is sugar for:
const isObject = v =>
v == null ? false : typeof v === 'function' || typeof v === 'object';
const p = { z: 30 };
{
let x, y;
if (isObject(p) && ({ x, y } = p) && x !== undefined && y !== undefined) {
console.log(`first p=${x + y}`);
} else {
let z;
if (isObject(p) && ({ z } = p) && z !== undefined) {
console.log(`second p=${z}`);
} else {
let rest;
if (
isObject(p) &&
([x, y, ...rest] = p) &&
x !== undefined &&
y !== undefined
) {
console.log('array', x, y);
} else {
console.log('default');
}
}
}
}
It uses a Golang if INIT;PREDICATE variable scope style, which progressively declares predicate bound variables as the block advances.
A ternary dispatch like this:
const res = { status: 401, err: 'unauth' };
const msg =
res is {status} && status < 400 ? `status=${status}`
: res is {err} ? `status=${status}, error=${error}`
: 'ok';
Is sugar for (optionally, it could be implemented with if-else for a less leaky scope of bound variables):
const res = { status: 401, err: 'unauth' };
let msg;
{
let status, err;
msg =
isObject(res) &&
({ status } = res) &&
status !== undefined &&
status < 400
? `status=${status}`
: isObject(res) && ({ err } = res) && err !== undefined
? `status=${status}, error=${err}`
: 'ok';
}
console.assert(msg === 'status=401, error=unauth');
The isObject() implementation is debatable, but destructuring apparently has no issues with functions, objects and arrays and isObject is just checking to make sure x is PATTERN does not throw if x is null or cannot be destructured.
Scoping of bound variables could also be achieved with () => {} blocks, if nesting is allowed, ie.
myFn( p is {x} ? x : p is {y} ? y : 0 )
Would de-sugar into
myFn( ((x,y) => (({x}=p) && x !==undefined ? x : ({y} = p) && y !== undefined ? y : 0) )() )
Or some other, less x/y leaky implementation.
The x is { PREDICATE -> ... } (arrow dispatch) implementation would be similar, however it requires in-context pairing between destructuring assignments predicates and the LHSO x, which could be ambiguous since the PREDICATE would not support syntax similar to destructuring declarations, which serves no purpose but it's ambiguous nonetheless.
// this:
const sq = p is {
{x} -> x ** 2,
{y} && y>1 -> y ** 2,
() -> {
const r = Math.random();
return r ** 2;
}
};
// is sugar for:
let sq;
{
let x,y;
sq =
({ x } = p) && x !== undefined
? x ** 2
: ({ y } = p) && y !== undefined && y>1
? y ** 2
: (() => {
const r = Math.random()
return r ** 2;
})();
}
// but here's the ambiguity: does {x} mean:
const sq = {x} ? ... : ...;
But then I believe {x} or [x] as predicates do not serve any known purpose so they could be supressed from the is arrow dispatch construct.
This looks like, to compare objects, you're destructuring and using ===. What about deeply nested objects? What about two objects whose [[Prototype]] are different but are otherwise the same? What about an object obj, and Object.create(obj)?
What you're describing is a distinct proposal in and of itself; one that would have to have advanced significantly for pattern matching to be based on it in any way.
I'm not comparing a single object, I'm destructuring a single object into vars (pattern) and testing their values.
This is what pattern matching is in Elixir, C#, Java and others. It's a different way, simpler and more elegant to implement the examples in this proposal's README.
Though @rodrigolive 's solution may have flaws, the motivation is clear and I share the same feeling with him. And I very agree that we could learn something from Java switch expression feature and C# pattern match.
The proposal has been updated in #174, and as such, this issue seems no longer relevant.
If you disagree, taking into account the updated proposal, please file a new issue.