├── .npmrc ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── package.json ├── .gitignore ├── LICENSE ├── spec.emu └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build spec 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: ljharb/actions/node/install@main 12 | name: 'nvm install lts/* && npm install' 13 | with: 14 | node-version: lts/* 15 | - run: npm run build 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: ljharb/actions/node/install@main 15 | name: 'nvm install lts/* && npm install' 16 | with: 17 | node-version: lts/* 18 | - run: npm run build 19 | - uses: JamesIves/github-pages-deploy-action@v4.3.3 20 | with: 21 | branch: gh-pages 22 | folder: build 23 | clean: true 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "template-for-proposals", 4 | "description": "A repository template for ECMAScript proposals.", 5 | "scripts": { 6 | "start": "npm run build-loose -- --watch", 7 | "build": "npm run build-loose -- --strict", 8 | "build-loose": "node -e 'fs.mkdirSync(\"build\", { recursive: true })' && ecmarkup --load-biblio @tc39/ecma262-biblio --verbose spec.emu build/index.html --lint-spec" 9 | }, 10 | "homepage": "https://github.com/tc39/template-for-proposals#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/tc39/template-for-proposals.git" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@tc39/ecma262-biblio": "2.1.2653", 18 | "ecmarkup": "^18.0.0" 19 | }, 20 | "engines": { 21 | "node": ">= 12" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Only apps should have lockfiles 40 | yarn.lock 41 | package-lock.json 42 | npm-shrinkwrap.json 43 | pnpm-lock.yaml 44 | 45 | # Build directory 46 | build 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ECMA TC39 and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec.emu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
  7 | title: Negated in and instanceof operators
  8 | stage: 1
  9 | contributors: Pablo Gorostiaga Belio
 10 | 
11 | 12 |

Relational Operators

13 | 14 |

The result of evaluating a relational operator is always of type Boolean, reflecting whether the relationship named by the operator holds between its two operands.

15 |
16 |

Syntax

17 | 18 | RelationalExpression[In, Yield, Await] : 19 | ShiftExpression[?Yield, ?Await] 20 | RelationalExpression[?In, ?Yield, ?Await] `<` ShiftExpression[?Yield, ?Await] 21 | RelationalExpression[?In, ?Yield, ?Await] `>` ShiftExpression[?Yield, ?Await] 22 | RelationalExpression[?In, ?Yield, ?Await] `<=` ShiftExpression[?Yield, ?Await] 23 | RelationalExpression[?In, ?Yield, ?Await] `>=` ShiftExpression[?Yield, ?Await] 24 | RelationalExpression[?In, ?Yield, ?Await] `instanceof` ShiftExpression[?Yield, ?Await] 25 | RelationalExpression[?In, ?Yield, ?Await] `!instanceof` ShiftExpression[?Yield, ?Await] 26 | [+In] RelationalExpression[+In, ?Yield, ?Await] `in` ShiftExpression[?Yield, ?Await] 27 | RelationalExpression[?In, ?Yield, ?Await] `!in` ShiftExpression[?Yield, ?Await] 28 | [+In] PrivateIdentifier `in` ShiftExpression[?Yield, ?Await] 29 | 30 | 31 |

The [In] grammar parameter is needed to avoid confusing the `in` operator in a relational expression with the `in` operator in a `for` statement.

32 |
33 | 34 | 35 |

Runtime Semantics: Evaluation

36 | RelationalExpression : RelationalExpression `<` ShiftExpression 37 | 38 | 1. Let _lref_ be ? Evaluation of |RelationalExpression|. 39 | 1. Let _lval_ be ? GetValue(_lref_). 40 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 41 | 1. Let _rval_ be ? GetValue(_rref_). 42 | 1. Let _r_ be ? IsLessThan(_lval_, _rval_, *true*). 43 | 1. If _r_ is *undefined*, return *false*. Otherwise, return _r_. 44 | 45 | RelationalExpression : RelationalExpression `>` ShiftExpression 46 | 47 | 1. Let _lref_ be ? Evaluation of |RelationalExpression|. 48 | 1. Let _lval_ be ? GetValue(_lref_). 49 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 50 | 1. Let _rval_ be ? GetValue(_rref_). 51 | 1. Let _r_ be ? IsLessThan(_rval_, _lval_, *false*). 52 | 1. If _r_ is *undefined*, return *false*. Otherwise, return _r_. 53 | 54 | RelationalExpression : RelationalExpression `<=` ShiftExpression 55 | 56 | 1. Let _lref_ be ? Evaluation of |RelationalExpression|. 57 | 1. Let _lval_ be ? GetValue(_lref_). 58 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 59 | 1. Let _rval_ be ? GetValue(_rref_). 60 | 1. Let _r_ be ? IsLessThan(_rval_, _lval_, *false*). 61 | 1. If _r_ is either *true* or *undefined*, return *false*. Otherwise, return *true*. 62 | 63 | RelationalExpression : RelationalExpression `>=` ShiftExpression 64 | 65 | 1. Let _lref_ be ? Evaluation of |RelationalExpression|. 66 | 1. Let _lval_ be ? GetValue(_lref_). 67 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 68 | 1. Let _rval_ be ? GetValue(_rref_). 69 | 1. Let _r_ be ? IsLessThan(_lval_, _rval_, *true*). 70 | 1. If _r_ is either *true* or *undefined*, return *false*. Otherwise, return *true*. 71 | 72 | RelationalExpression : RelationalExpression `instanceof` ShiftExpression 73 | 74 | 1. Let _lref_ be ? Evaluation of |RelationalExpression|. 75 | 1. Let _lval_ be ? GetValue(_lref_). 76 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 77 | 1. Let _rval_ be ? GetValue(_rref_). 78 | 1. Return ? InstanceofOperator(_lval_, _rval_). 79 | 80 | 81 | RelationalExpression : RelationalExpression `!instanceof` ShiftExpression 82 | 83 | 1. Let _lref_ be ? Evaluation of |RelationalExpression|. 84 | 1. Let _lval_ be ? GetValue(_lref_). 85 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 86 | 1. Let _rval_ be ? GetValue(_rref_). 87 | 1. Let _r_ be ? InstanceofOperator(_lval_, _rval_). 88 | 1. If _r_ is *true*, return *false*. Otherwise, return *true*. 89 | 90 | 91 | RelationalExpression : RelationalExpression `in` ShiftExpression 92 | 93 | 1. Let _lref_ be ? Evaluation of |RelationalExpression|. 94 | 1. Let _lval_ be ? GetValue(_lref_). 95 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 96 | 1. Let _rval_ be ? GetValue(_rref_). 97 | 1. If _rval_ is not an Object, throw a *TypeError* exception. 98 | 1. Return ? HasProperty(_rval_, ? ToPropertyKey(_lval_)). 99 | 100 | 101 | RelationalExpression : RelationalExpression `!in` ShiftExpression 102 | 103 | 1. Let _lref_ be ? Evaluation of |RelationalExpression|. 104 | 1. Let _lval_ be ? GetValue(_lref_). 105 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 106 | 1. Let _rval_ be ? GetValue(_rref_). 107 | 1. If _rval_ is not an Object, throw a *TypeError* exception. 108 | 1. Let _r_ be ? HasProperty(_rval_, ? ToPropertyKey(_lval_)). 109 | 1. If _r_ is *true*, return *false*. Otherwise, return *true*. 110 | 111 | 112 | RelationalExpression : PrivateIdentifier `in` ShiftExpression 113 | 114 | 1. Let _privateIdentifier_ be the StringValue of |PrivateIdentifier|. 115 | 1. Let _rref_ be ? Evaluation of |ShiftExpression|. 116 | 1. Let _rval_ be ? GetValue(_rref_). 117 | 1. If _rval_ is not an Object, throw a *TypeError* exception. 118 | 1. Let _privateEnv_ be the running execution context's PrivateEnvironment. 119 | 1. Let _privateName_ be ResolvePrivateIdentifier(_privateEnv_, _privateIdentifier_). 120 | 1. If PrivateElementFind(_rval_, _privateName_) is not ~empty~, return *true*. 121 | 1. Return *false*. 122 | 123 |
124 |
125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Negated `in` and `instanceof` operators 2 | 3 | ## Status 4 | 5 | **Stage**: 1 6 | **Champion**: Pablo Gorostiaga Belio ([@gorosgobe](https://github.com/gorosgobe)) 7 | 8 | ## Author 9 | Pablo Gorostiaga Belio ([@gorosgobe](https://github.com/gorosgobe)) 10 | 11 | # Proposal 12 | 13 | Presentations 14 | 15 | - [September 2023](https://docs.google.com/presentation/d/1vwNOjUiUvy6TzK6t0Mb8qgyKwBNszly9HibJJpUa_Eo/edit) 16 | 17 | ## Motivation 18 | 19 | JavaScript's `in` and `instanceof` operators have broadly the following behaviour: 20 | 21 | ```js 22 | a in obj; // returns true if property a is in obj or in its prototype chain, false otherwise 23 | a instanceof C; // returns true if C.prototype is in a's prototype chain, false otherwise 24 | ``` 25 | 26 | To negate the result of these expressions, we can wrap them with the logical NOT (`!`) operator: 27 | 28 | ```js 29 | !(a in obj); 30 | !(a instanceof C); 31 | ``` 32 | 33 | Negating an `in`/`instanceof` expression in this way suffers from a few problems: 34 | 35 | ### Error-proneness[^1] 36 | 37 | The logical not operator, `!`, has to be applied to the whole expression to produce the intended result. Incorrect parenthesising of the sub-expression (which can be a part of an arbitrarily long expression) and/or applying the `!` operator on the wrong operand can lead to errors that are hard to debug, i.e.: 38 | 39 | [^1]: A note about TypeScript: error-proneness is less of a concern if TypeScript is used, because TypeScript checks that the correct types are passed to the `in` and `instanceof` operators. However, incorrect or lack of types can still cause this issue. This can also happen if you don't use TypeScript, or if that particular part of your code is untyped or uses `any` explicitly. 40 | 41 | For `in`: 42 | ```js 43 | if (!a in obj) { 44 | // will not execute, unless obj has a 'true' or 'false' key 45 | // `in` accepts strings or symbols as the LHS parameter, and otherwise coerces all other values to a string 46 | // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion 47 | } 48 | 49 | // correct usage 50 | if (!(a in obj)) { 51 | // ... 52 | } 53 | ``` 54 | For `instanceof`: 55 | ```js 56 | if (!a instanceof C) { 57 | // will not execute, unless C provides a @@hasInstance method that returns true for booleans 58 | } 59 | 60 | // correct usage 61 | if (!(a instanceof C)) { 62 | // ... 63 | } 64 | ``` 65 | 66 | This type of error is fairly common. For `in`, this [Sourcegraph query](https://sourcegraph.com/search?q=context:global+lang:javascript+/%5C%21%5B%5B:alnum:%5D%5C%27%5C%22%5D%2B+in+%5B%5B:alnum:%5D%5D%2B/+-file:%5C.min%5C.js%24+count:all&patternType=standard&sm=1&groupBy=repo) reveals that there are many instances of this issue (over ~2.1k instances when I ran it) across repos with thousands of stars on GitHub. While there are some false positives (from comments, for example), I highlight some notable examples below: 67 |
68 | Examples 69 | 70 | |Repo |Bugs |Stars |Link|Issue| 71 | |--------------------------------------------------|---------------------------------------------------------------|------|----|-----| 72 | |[meteor/meteor](https://github.com/meteor/meteor) |`!key in validDevices` |43.6k | [Link](https://github.com/meteor/meteor/blob/57759e09746046fb75cbd1479d72cda30bba081f/tools/cordova/builder.js#L803)| [Issue](https://github.com/meteor/meteor/issues/12781) | 73 | |[oven-sh/bun](https://github.com/oven-sh/bun) |`!"TZ" in process.env` |42.7k |[Link](https://github.com/oven-sh/bun/blob/6bfee02301a2e2a0b79339974af0445eb5a2688f/test/js/node/process/process.test.js#L106)| [Issue](https://github.com/oven-sh/bun/issues/5800) | 74 | |[SergioBenitez/Rocket](https://github.com/SergioBenitez/Rocket) |`!"message" in msg \|\| !"room" in msg \|\| !"username" in MSG` |21.1k| [Link](https://github.com/SergioBenitez/Rocket/blob/c2936fcb1e4f8f4907889b54a9e4e741a565a7d7/examples/chat/static/script.js#L97)| [Issue](https://github.com/SergioBenitez/Rocket/issues/2617) | 75 | |[jeromeetienne/AR.js](https://github.com/jeromeetienne/AR.js) |`!'VRFrameData' in window` |15.7k |[Link](https://github.com/jeromeetienne/AR.js/blob/024318c67121bd57045186b83b42f10c6560a34a/three.js/examples/vendor/webvr-polyfill.js#L6245)| [Issue](https://github.com/jeromeetienne/AR.js/issues/828) | 76 | |[duplicati/duplicati](https://github.com/duplicati/duplicati) |`!'IsUnencryptedOrPassphraseStored' in this.Backup` |9.1k |[Link](https://github.com/duplicati/duplicati/blob/d0f1498bd41b151d8512fd2acb57739f6a05587f/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js#L456)| [Issue](https://github.com/duplicati/duplicati/issues/5028) | 77 | |[WebKit/WebKit](https://github.com/WebKit/WebKit) |`!'openDatabase' in window` |6.4k |[Link](https://github.com/WebKit/WebKit/blob/21506dd04e3ba5815f62bfd714acd73ce48ca3ef/Tools/CSSTestSuiteHarness/harness/harness.js#L1477)| [Issue](https://bugs.webkit.org/show_bug.cgi?id=261815) | 78 | |[buildbot/buildbot](https://github.com/buildbot/buildbot) |`!option in options` |5.1k |[Link](https://github.com/buildbot/buildbot/blob/4cf871e7378e87a5e9b811e764874645e14b57ab/www/build_common/src/webpack.js#L21)| [Issue](https://github.com/buildbot/buildbot/issues/7120) | 79 | |[cloudflare/workerd](https://github.com/cloudflare/workerd) |`!type in this.#recipes` |4.9k |[Link](https://github.com/cloudflare/workerd/blob/30eb1e66be0aff8ce9c6f9ec76ac2548a2bf4247/samples/extensions/burrito-shop-impl.js#L19)| [Issue](https://github.com/cloudflare/workerd/issues/1207) | 80 | |[muicss/mui](https://github.com/muicss/mui) |`!'rows' in rest` |4.5k |[Link](https://github.com/muicss/mui/blob/d1774138e025f99c870f9dbb556163028cc2d475/src/react/textarea.jsx#L21)| [Issue](https://github.com/muicss/mui/issues/336) | 81 | |[jlord/git-it-electron](https://github.com/jlord/git-it-electron) |`!'previous' in curCommit` |4.4k |[Link](https://github.com/jlord/git-it-electron/blob/e7551c58366787dbc62ee7ef6079fc8cc6c7acb9/assets/PortableGit/mingw32/share/gitweb/static/gitweb.js#L1374)| [Issue](https://github.com/jlord/git-it-electron/issues/393) | 82 | |[zlt2000/microservices-platform](https://github.com/zlt2000/microservices-platform) |`!'onhashchange' in W` |4.2k |[Link](https://github.com/zlt2000/microservices-platform/blob/da821d6b598fb82d901fc67861cf902b3e60389c/zlt-web/layui-web/src/main/resources/static/assets/libs/q.js#L38)| [Issue](https://github.com/zlt2000/microservices-platform/issues/67) | 83 | |[thechangelog/changelog.com](https://github.com/thechangelog/changelog.com) |`!"execCommand" in document` |2.6k| [Link](https://github.com/thechangelog/changelog.com/blob/271286cc8ae68298755bf08a68d1af02dd016603/assets/app/modules/onsitePlayer.js#L449)| [Issue](https://github.com/thechangelog/changelog.com/issues/483) | 84 | |[kiwibrowser/src](https://github.com/kiwibrowser/src) |`!intervalName in this.intervals` |2.3k| [Link](https://github.com/kiwibrowser/src/blob/945c26be7a1e458cef098d8d782b5555611cc83b/components/chrome_apps/webstore_widget/app/main.js#L105)| [Issue](https://github.com/kiwibrowser/android/issues/285) | 85 | |[drawcall/Proton](https://github.com/drawcall/Proton) |`!'defineProperty' in Object` |2.3k| [Link](https://github.com/drawcall/Proton/blob/83c3caa8203c4e60c7363fb3fffbfd69c9d7ba0e/example/game/crafty/js/crafty.js#L4306)| [Issue](https://github.com/drawcall/Proton/issues/101) | 86 | |[montagejs/collections](https://github.com/montagejs/collections) |`!index in this` |2.1k| [Link](https://github.com/montagejs/collections/blob/4e19cc48904dbc6313dbe9199f347969843d2308/shim-array.js#L106)| [Issue](https://github.com/montagejs/collections/issues/252) | 87 | 88 |
89 | 90 | Similarly, for `instanceof`, this [Sourcegraph query](https://sourcegraph.com/search?q=context:global+lang:javascript+/%5C%21%5B%5B:alnum:%5D%5D%2B+instanceof+%5B%5B:alnum:%5D%5D%2B/+-file:%5C.min%5C.js%24+count:all&patternType=standard&sm=1&groupBy=repo) shows that there are also many instances of this bug (~19k occurrences when I ran it). As before, repos with thousands of stars are affected. Some examples follow below: 91 | 92 |
93 | Examples 94 | 95 | |Repo |Bugs |Stars |Link|Issue| 96 | |--------------------------------------------------|---------------------------------------------------------------|------|----|-----| 97 | |[odoo/odoo](https://github.com/odoo/odoo) |`!e instanceof o` |30.1k |[Link](https://github.com/odoo/odoo/blob/a05ccee899c85c95bc82fb1a8e9c0dd4b3fd5a5c/addons/web/static/lib/ace/ace.odoo-custom.js#L3393)| [Issue](https://github.com/odoo/odoo/issues/136022) | 98 | |[facebook/flow](https://github.com/facebook/flow) |`!flow instanceof RegExp` |22k |[Link](https://github.com/facebook/flow/blob/469629e78c90ab2da056e632f02edf56a580cf86/packages/flow-parser/test/esprima_test_runner.js#L449)| [Issue](https://github.com/facebook/flow/issues/9082) | 99 | |[v8/v8](https://github.com/v8/v8) |`!e instanceof RangeError` |21.5k |[Link](https://github.com/v8/v8/blob/29229448d9f57735d850bc49697a678c4e0a6925/test/js-perf-test/ExpressionDepth/run.js#L54) | [Issue](https://bugs.chromium.org/p/v8/issues/detail?id=14329) | 100 | |[linlinjava/litemall](https://github.com/linlinjava/litemall) |`!re instanceof RegExp` |18.2k |[Link](https://github.com/linlinjava/litemall/blob/47ea5c7420f126081e7ef17a7182890def32457d/renard-wx/lib/wxParse/showdown.js#L2238)| [Issue](https://github.com/linlinjava/litemall/issues/542) | 101 | |[iissnan/hexo-theme-next](https://github.com/iissnan/hexo-theme-next) |`!elem instanceof Element` |15.8k |[Link](https://github.com/iissnan/hexo-theme-next/blob/9c8cea69bf0d4f91c07779d71b01814b27bbb6a1/source/lib/Han/dist/han.js#L2154)| [Issue](https://github.com/iissnan/hexo-theme-next/issues/2270) | 102 | |[chromium/chromium](https://github.com/chromium/chromium) |`!this instanceof Test` |15.3k |[Link](https://github.com/chromium/chromium/blob/ec7efe1a70a533678591c239f61fba591db9bee5/third_party/qunit/src/qunit.js#L1070)| [Issue](https://bugs.chromium.org/p/chromium/issues/detail?id=1485073) | 103 | |[arangodb/arangodb](https://github.com/arangodb/arangodb) |`!context instanceof WebGLRenderingContext` |13.1k |[Link](https://github.com/arangodb/arangodb/blob/1e050ea77f258045aa0bb4b1c3b1ebc60709d29d/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.exporters.image.js#L295)| [Issue](https://github.com/arangodb/arangodb/issues/19795) | 104 | |[ptmt/react-native-macos](https://github.com/ptmt/react-native-macos) |`!response instanceof Map` |11.3k |[Link](https://github.com/ptmt/react-native-macos/blob/0f09ff48a8c1e2310ec9eef2529d64e321c0b599/local-cli/server/util/jsPackagerClient.js#L99)| N/A (deprecated) | 105 | |[chakra-core/ChakraCore](https://github.com/chakra-core/ChakraCore) |`!e instanceof TypeError` |8.9k |[Link](https://github.com/chakra-core/ChakraCore/blob/c3ead3f8a6e0bb8e32e043adc091c68cba5935e9/test/Array/array_splice.js#L124)| [Issue](https://github.com/chakra-core/ChakraCore/issues/6950) | 106 | |[icindy/wxParse](https://github.com/icindy/wxParse) |`!ext.regex instanceof RegExp` |7.7k |[Link](https://github.com/icindy/wxParse/blob/9d5df482294b7d39f8802d413f25d28d0d6c349e/wxParse/showdown.js#L397)| [Issue](https://github.com/icindy/wxParse/issues/378) | 107 | |[WebKit/WebKit](https://github.com/WebKit/WebKit) |`!e instanceof Error` |6.4k |[Link](https://github.com/WebKit/WebKit/blob/ea191c94955ddd2f015f7a677b138109987620b2/JSTests/stress/spread-calling.js#L78)| [Issue](https://bugs.webkit.org/show_bug.cgi?id=261815) | 108 | |[golden-layout/golden-layout](https://github.com/golden-layout/golden-layout) |`!column instanceof lm.items.RowOrColumn` |6k |[Link](https://github.com/golden-layout/golden-layout/blob/95af36d1e4c596696483c5353d32ae71886b999c/website/assets/js/goldenlayout.js#L3729)| [Issue](https://github.com/golden-layout/golden-layout/issues/855) | 109 | |[janhuenermann/neurojs](https://github.com/janhuenermann/neurojs) |`!config instanceof network.Configuration` |4.4k |[Link](https://github.com/janhuenermann/neurojs/blob/9a19adc2c3d56a4276affa06fb61524dca5bbbd9/src/storage.js#L36)| [Issue](https://github.com/janhuenermann/neurojs/issues/21) | 110 | |[gkz/LiveScript](https://github.com/gkz/LiveScript) |`!last instanceof While` |2.3k |[Link](https://github.com/gkz/LiveScript/blob/6f754f9c51d133efa8a33504157db4c059ea23c1/lib/ast.js#L3953)| [Issue](https://github.com/gkz/LiveScript/issues/1123) | 111 | |[CloudBoost/cloudboost](https://github.com/CloudBoost/cloudboost) |`!obj instanceof CB.CloudObject \|\| !obj instanceof CB.CloudFile \|\| !obj instanceof CB.CloudGeoPoint \|\| !obj instanceof CB.CloudTable \|\| !obj instanceof CB.Column` |1.4k |[Link](https://github.com/CloudBoost/cloudboost/blob/aa4564056047fb6ec804590f323b7f0a2a010e3b/data-service/sdk/src/PrivateMethods.js#L66)| [Issue](https://github.com/CloudBoost/cloudboost/issues/494) | 112 | 113 | 114 |
115 | 116 | Within Bloomberg, we encourage the use of eslint and TypeScript, each of which have an error for these cases. However, because we allow teams to make some of their own decisions about tooling, bugs creeped through: in one large set of internal projects, we found that roughly an eighth of `in`/`instanceof` usages were negated `in` and `instanceof` expressions. More than 1% of negated `in` uses had this bug. This also affected negated `instanceof`, where more than 6% of uses had the bug. Our internal results are aligned with the data from the external sourcegraph queries: there is clearly a higher incidence of the bug on negated `instanceof` expressions compared to negated `in` expressions. While we are now fixing this internally, overall these results illustrate that this is a common problem due to the lack of ergonomics around negated `in` and `instanceof` expressions. 117 | 118 | ### Generates confusion 119 | 120 | The negation of these expressions is not aligned with operators which have a negated version, such as `===`/`!==`. This generates confusion among developers and leads to highly upvoted and viewed questions such as [Is there a “not in” operator in JavaScript for checking object properties?](https://stackoverflow.com/questions/7972446/is-there-a-not-in-operator-in-javascript-for-checking-object-properties) and [Javascript !instanceof If Statement](https://stackoverflow.com/questions/8875878/javascript-instanceof-if-statement). 121 | 122 | ### Readability 123 | 124 | To negate the result of an `in`/`instanceof` expression, we introduce an additional grouping operator (denoted by two parentheses). In addition, the `not` is at the beginning of the expression, unlike how this would be read in natural English. Together, both of these factors result in less readable code. 125 | 126 | ### Worse developer experience 127 | 128 | It is common to use `in`/`instanceof` as a guard in conditionals. Inverting these conditionals to reduce indentation in code, as [this is correlated with code complexity](https://www.sciencedirect.com/science/article/pii/S0167642309000379), can lead to improved code readability and quality. With the existing operators, inverting the expression in the conditional requires the expression to be both wrapped with parentheses **and** negated. 129 | 130 | ## Solution 131 | 132 | `!in`, a negated version of `in`, where 133 | 134 | ```js 135 | a !in obj; 136 | ``` 137 | 138 | is equivalent to 139 | ```js 140 | !(a in obj); 141 | ``` 142 | 143 | `!instanceof`, a negated version of `instanceof`, where 144 | 145 | ```js 146 | a !instanceof obj; 147 | ``` 148 | 149 | is equivalent to 150 | ```js 151 | !(a instanceof obj); 152 | ``` 153 | 154 | - Safer: No longer need to introduce additional grouping, and the negation is applied directly to the operator, as opposed to applying it next to the LHS operand in the expression. 155 | - Improved readability: No longer requires extra grouping to negate the result of the expression. This is aligned with other operators such as `!==`. Reads more naturally and is more intuitive. 156 | - Better developer experience: Again, easier to change when refactoring code - a single `!` needs to be added to negate the expression. 157 | 158 | ## In other languages: 159 | 160 | Python: 161 | ```python 162 | if item not in items: 163 | pass 164 | 165 | if ref1 is not ref2: 166 | pass 167 | ``` 168 | 169 | Kotlin: 170 | ```kotlin 171 | if (a !in arr) {} 172 | 173 | if (a !is SomeClass) {} 174 | ``` 175 | 176 | C#: 177 | ```csharp 178 | if (a is not null) {} 179 | ``` 180 | 181 | Elixir: 182 | ```elixir 183 | a not in [1, 2, 3] 184 | ``` 185 | 186 | ## Related Proposals 187 | 188 | ### Pattern matching 189 | The pattern matching proposal proposes a new relational expression like `a in b` or `a instanceof b`, using a new operator `is`: https://github.com/tc39/proposal-pattern-matching#is-expression 190 | 191 | In the same line as `in` and `instanceof`, we could extend the proposal to include a negated `is` operator such as `!is`. 192 | --------------------------------------------------------------------------------