{"id":21140251,"url":"https://github.com/wlib/proposal-switch-match","last_synced_at":"2026-01-02T02:35:56.572Z","repository":{"id":219039198,"uuid":"278215490","full_name":"wlib/proposal-switch-match","owner":"wlib","description":"An ECMAScript proposal for pattern matching","archived":false,"fork":false,"pushed_at":"2020-07-10T22:57:02.000Z","size":12,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-01-21T06:44:59.173Z","etag":null,"topics":["ecmascript","pattern-matching","proposal","tc39"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wlib.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-07-08T23:30:37.000Z","updated_at":"2020-07-10T22:57:04.000Z","dependencies_parsed_at":"2024-01-25T05:48:12.697Z","dependency_job_id":null,"html_url":"https://github.com/wlib/proposal-switch-match","commit_stats":null,"previous_names":["wlib/proposal-switch-match"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wlib%2Fproposal-switch-match","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wlib%2Fproposal-switch-match/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wlib%2Fproposal-switch-match/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wlib%2Fproposal-switch-match/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wlib","download_url":"https://codeload.github.com/wlib/proposal-switch-match/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243581092,"owners_count":20314167,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ecmascript","pattern-matching","proposal","tc39"],"created_at":"2024-11-20T07:13:28.946Z","updated_at":"2026-01-02T02:35:56.528Z","avatar_url":"https://github.com/wlib.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Proposed New Approach for Pattern Matching in ECMAScript\nAuthor: Daniel Ethridge ([@wlib](https://git.io/de)).\n## Introduction\nThe entire purpose of pattern matching is to branch logic. We currently use the\n`if` statement and the `switch` statement to do this. The issue is that neither\nof these constructs are ergonomic. `if` statements are usually ad hoc and using\n`switch` is usually avoided because of its verbosity and unintuitive semantics.\nWe can solve these problems by reapproaching this issue from better principles.\n\nFirst, understand that the main purpose of pattern matching is to take a value,\nand to do something based on what that value is. In all cases, this can be just\nseen as [elimination of sum types](https://en.wikipedia.org/wiki/Tagged_union).\n\nThis, in other words, means that a pattern matching solution needs to know about types.\n\n### Types?\nFor better or worse, ECMAScript is dynamically and weakly typed (this can be\nbetter described as [unityped](http://lists.seas.upenn.edu/pipermail/types-list/2014/001733.html)).\nA result of this is that pattern matching is an inherently foreign concept within the language.\n\nFortunately, there is a solution to this. Here's a hint - destructuring:\n\n```javascript\nconst someObject =\n  { foo: 1\n  , bar: [ \"thank\", \"you\", \"tc39\", \"very\", \"cool\" ]\n  , bruh: true\n  }\n\nconst { bruh, bar: [ , , ...message] } = someObject\n\nconsole.log(bruh)    // true\nconsole.log(message) // [\"tc39\", \"very\", \"cool\"]\n```\n\n[Destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)\nin ECMAScript is structural. If the left hand side of the assignment\n(the `{ bruh, bar: [ , , ...message] }` part) is a substructure of the right hand side,\nthe sides match and the destructuring assignment is successful.\n\nThis basically means that ECMAScript already has extensive support for\n[duck typing](https://en.wikipedia.org/wiki/Duck_typing).\n\nBut on top of that, the language also somewhat supports\n[nominal typing](https://en.wikipedia.org/wiki/Nominal_type_system) using the\n[`instanceof` operator](https://javascript.info/instanceof):\n\n```javascript\nclass Moment extends Date {}\nclass Bruh extends Moment {}\n\nconst bruh = new Bruh()\n\nconsole.log(bruh instanceof Bruh)   // true\nconsole.log(bruh instanceof Moment) // true\nconsole.log(bruh instanceof Date)   // true\nconsole.log(bruh instanceof Object) // true\n```\n\n...and on top of that the language has all kinds of arbitrary \"types\" that\nneed to be checked. For example, the only way to tell if a number is natural\nis with a function:\n\n```javascript\nconst isNaturalNumber = n =\u003e\n  Number.isInteger(n) \u0026\u0026 n \u003e= 0\n```\n\n## Proposal\nSo our solution for pattern matching should incorporate both destructuring and\narbitrary matching in order to be maximally useful. It can be understood as\njust a newer `switch` statement that allows matching if the value...\n\n* ...is strictly equal (`===`) to a case, like a normal `switch` would.\n* ...can be destructured by a case.\n* ...fits some arbitrarily chosen criteria.\n\nFinally, our solution must abandon the current `switch` syntax in order to be\nmore consistent with the syntax of all of the other statements.\n\nHere is roughly what its syntax looks like:\n\n```\nswitch* (\u003cexpression\u003e [; \u003cfunction\u003e]) {\n  case ([\u003cexpression\u003e | \u003cdestructuring\u003e]...) \u003cblock\u003e\n  ...\n  default \u003cblock\u003e\n}\n```\n\n## Rationale\n\n### Switch Asterisk\nUsing a `switch` followed by an asterisk is exactly the same approach that was taken when\n[generator functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*)\nwhere introduced in order to maintain backwards compatibility without taking a\nnew keyword. This decision could arbitrarily be changed to introduce and use a\nkeyword like `match`, but `switch *` will work without any unnecessary hassle.\n\n### No Fall-Through\nUnlike the classic `switch` statement, there is no fallthrough. This is\nbecause fall-through is an unnecessary source of confusion.\nHere's an example of counterintuitive fall-through behavior:\n\n```javascript\nswitch (1) {\n  case 0: console.log(0)\n  default: console.log(\"default\")\n  case 2: console.log(2)\n}\n// default\n// 2\n\nswitch (1) {\n  case 0: console.log(0)\n  case 1: console.log(1)\n  default: console.log(\"default\")\n  case 2: console.log(2)\n}\n// 1\n// default\n// 2\n```\n\nBut of course - the most common mistake is to forget to write all of the `break`'s.\nNow, rather than fall-through, you can match one of multiple patterns like so:\n\n```javascript\nconst userInput = \"hello\"\n\nswitch* (userInput) {\n  case (\"hi\", \"hello\", \"hey\")\n    console.log(`Oh, ${userInput}!`)\n  \n  case (\"bye\", \"goodbye\", \"see ya\")\n    console.log(\"Have a good day!\")\n\n  default\n    console.log(\"Sorry, I don't understand that much...\")\n}\n```\n\n### Optional Custom Match Function\nNormally, a `case` clause only matches using strict equality or a destructuring\nassignment. When matching another expression (when not destructuring), the\nmatching function is equivalent to `(a, b) =\u003e a === b`. This can be too rigid\nfor many cases in pattern matching, so an optional custom match function is\nsupported. This is especially useful for ranges or custom logic:\n\n```javascript\n// Contrived example that tests is two numbers are coprime\nconst gcd = (a, b) =\u003e\n  b == 0 ? Math.abs(a)\n         : gcd(b, a % b)\n\nconst isCoPrimeTo = (a, b) =\u003e\n  gcd(a, b) === 1\n\nswitch* (14; isCoPrimeTo) {\n  case (2, 4, 6, 8)\n    console.log(\"This shouldn't happen\")\n\n  case (15)\n    console.log(\"This should happen\")\n}\n\n// Arbitrary pattern matching\nconst isLessThan = (a, b) =\u003e a \u003c b\n\nswitch* (someNumber; isLessThan) {\n  case (0)\n    console.log(\"Negative\")\n\n  case (10)\n    console.log(\"Single Digits\")\n  \n  case (50)\n    console.log(\"Under 50\")\n\n  default\n    console.log(\"50 or more?\")\n}\n```\n\n### Blocks\nA common pattern in ECMAScript is to use blocks:\n\n```javascript\n// Blocks can use { } to hold multiple statements\nif (false) { \n  block;\n  fullOf;\n  statements;\n}\n\nwhile (false) {\n  doNothingIGuess()\n}\n\nfor (let i = 0; i \u003c 3; i++) {\n  doSomethingTo(i)\n}\n\nlambda = argument =\u003e {\n  return result(argument)\n}\n\n// Or ignore the { } for just one statement\nif (false)\n  singleStatement;\nelse if (false)\n  singleStatement\nelse\n  singleStatement\n\nwhile (false)\n  doNothingIGuess()\n\nfor (let i = 0; i \u003c 3; i++)\n  doSomethingTo(i)\n\nlambda = argument =\u003e\n  result(argument)\n```\n\nNow, the clauses within `switch*` behave the same way, making them much\nmore consistent with intuition for how pattern matching *should* work:\n\n```javascript\nconst map = (f, iterable) =\u003e {\n  switch* (iterable) {\n    case ([])\n      return []\n\n    case ([x, ...xs])\n      return [f(x), ...map(f, xs)]\n\n    default {\n      console.error(\"You cannot pass\", iterable, \"into the map function\")\n      throw new TypeError(\"Not an Iterable\")\n    }\n  }\n}\n```\n\n## More Examples\n\nMatching sum types (though undeclared and not compile-time checked):\n\n```javascript\nconst isInstanceOf = (a, b) =\u003e a instanceof b\n\nconst bTreeSize = tree =\u003e {\n  switch* (tree; isInstanceOf) {\n    case (Leaf)\n      return 1\n\n    case (Node)\n      return bTreeSize(Node.left) + bTreeSize(Node.right)\n  }\n}\n\nconst apply = (f, maybeSomething) =\u003e {\n  switch* (maybeSomething; isInstanceOf) {\n    case (Just)\n      return f(maybeSomething.value)\n\n    case (Nothing)\n      return maybeSomething\n\n    default\n      throw new TypeError(maybeSomething, \"needs to be a Maybe type\")\n  }\n}\n```\n\nThis enables really creative pattern matching \"types/sort of typeclasses\":\n\n```javascript\nclass Functor {\n  static [Symbol.hasInstance](possibleFunctor) {\n    return typeof possibleFunctor.map === \"function\"\n  }\n}\n\nconst myArray = [1, 2, 3, 4, 5]\nconst isInstanceOf = (a, b) =\u003e a instanceof b\n\nswitch* (myArray; isInstanceOf) {\n  case (Functor) {\n    console.log(\"This will match, because arrays are functors\")\n    console.log( myArray.map(n =\u003e n * 2) )\n  }\n}\n```\n\n## Caveats\nThere is an inherent ambiguity whether or not a case clause contains an\nexpression or a destructuring assignment. For example, what should this be?\n\n```javascript\nconst stuff = [1]\n\nswitch* (someIterable) {\n  case ([]) {\n    // Is this an expression (match empty array) or destructuring?\n  }\n  case ([...stuff]) {\n    // Does this match [1] or destructure?\n  }\n  case ([stuff]) {\n    // Does this destructure or match [[1]]?\n  }\n  case (stuff) {\n    // Does this destructure assign without actually destructuring?\n    // Or does it evaluate as an expression to match [1]?\n  }\n}\n```\n\nThis can be resolved by treating anything that can be parsed as a\ndestructuring assignment as a destructuring assignment. This includes\nall `{}` and `[]`, but excludes bare identifiers and any other expressions.\nSo every clause except the last one is a destructuring match, while the last\nclause matches the variable `stuff` itself.\n\nEssentially, just remember that `case` clauses take identifiers if you need to\nmatch an object or iterable.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwlib%2Fproposal-switch-match","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwlib%2Fproposal-switch-match","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwlib%2Fproposal-switch-match/lists"}