|
1 | | -# proposal-plus-minus-spread |
| 1 | +# Key exclusion and inclusion syntax in object spread |
2 | 2 |
|
3 | | -Minus and plus operators: a spread operator enhancement proposal for JavaScript / ECMAScript |
| 3 | +ECMAScript proposal and reference implementation for exclusion and inclusion syntax in the object spread. |
| 4 | + |
| 5 | +**Author(s):** Denis Tokarev (Canva) |
| 6 | + |
| 7 | +**Champion:** not identified |
| 8 | + |
| 9 | +**Stage:** 0 |
4 | 10 |
|
5 | 11 |
|
6 | 12 | ## Motivation |
7 | 13 |
|
8 | | -Since the object spread operator spec had landed, it has become widely used by devs. The reason for that is quite obvious – it's declarative, easily readable and straightforward way to produce an object from other objects. However, there are still use cases when devs have to write imperative code for performing quite simple operations. An example of such operation is filtering out an item from an object by key. |
| 14 | +Since its introduction to the specification, [the object spread syntax](https://github.com/tc39/proposal-object-rest-spread) has gained extreme popularity in the codebases of most organizations and open-source projects. Being declarative, the object spread syntax is easy to use, read, understand, and maintain. |
| 15 | + |
| 16 | +However, when the use case is slightly more complex than just merging a few objects, the developers don't have the luxury of writing declarative code. |
9 | 17 |
|
10 | | -Let's illustrate this wth code. Let's assume we've got an object `obj`: |
| 18 | +Perhaps the most popular example is removing a key from the result object: |
11 | 19 |
|
12 | 20 | ```js |
13 | | -const obj = { |
14 | | - a: 1, |
15 | | - b: { |
16 | | - b1: 21, |
17 | | - b2: 22, |
18 | | - }, |
19 | | - c: 3, |
| 21 | +// When the key name is known statically |
| 22 | +const sanitizedOpts = (opts) => { |
| 23 | + const result = { |
| 24 | + ...PRIVATE_OPTS, |
| 25 | + ..opts, |
| 26 | + }; |
| 27 | + |
| 28 | + // Removing the key "keyThatMustNotBeThere" from the result |
| 29 | + delete result.keyThatMustNotBeThere; |
| 30 | + |
| 31 | + return result; |
20 | 32 | }; |
21 | | -``` |
22 | 33 |
|
23 | | -If we want to take out an item from `obj.b` by its key (eg. `b1`) using a declarative paradigm, we have to write something like that (let's write it as a function, to cover a broader case when it does something meaningful before returning a result): |
| 34 | +// When there are multiple key names known statically |
| 35 | +const sanitizedOpts = (opts) => { |
| 36 | + const result = { |
| 37 | + ...PRIVATE_OPTS, |
| 38 | + ..opts, |
| 39 | + }; |
24 | 40 |
|
25 | | -```js |
26 | | -const removeB1 = (src) => ({ |
27 | | - ...src, |
28 | | - b: Object.keys(src.b).reduce((acc, key) => key === 'b1' ? acc : { ...acc, [key]: src.b[key] }, {}), |
29 | | -}); |
| 41 | + // Removing the key "keyThatMustNotBeThere" from the result |
| 42 | + delete result.keyThatMustNotBeThere; |
| 43 | + delete result.keyThatAlsoMustNotBeThere; |
| 44 | + |
| 45 | + return result; |
| 46 | +}; |
| 47 | + |
| 48 | +// When the key name is not known beforehand |
| 49 | +const sanitizedOpts = (opts) => { |
| 50 | + const result = { |
| 51 | + ...PRIVATE_OPTS, |
| 52 | + ..opts, |
| 53 | + }; |
| 54 | + |
| 55 | + // Removing the key stored in KEY_THAT_MUST_NOT_BE_THERE from the result |
| 56 | + delete result[KEY_THAT_MUST_NOT_BE_THERE]; |
| 57 | + |
| 58 | + return result; |
| 59 | +}; |
| 60 | + |
| 61 | +// When there are multiple keys to remove |
| 62 | +const sanitizedOpts = (opts) => { |
| 63 | + const result = { |
| 64 | + ...PRIVATE_OPTS, |
| 65 | + ..opts, |
| 66 | + }; |
| 67 | + |
| 68 | + // Removing all the key names stored in KEYS_TO_REMOVE from the result |
| 69 | + KEYS_TO_REMOVE.forEach((key) => { |
| 70 | + delete result[key]; |
| 71 | + }); |
| 72 | + |
| 73 | + return result; |
| 74 | +}; |
30 | 75 | ``` |
31 | 76 |
|
32 | | -This line, in particular, requires some time to understand what's going on: |
| 77 | +Removing keys this way has a few significant disadvantages: |
| 78 | +- It is wordy. |
| 79 | +- It is non-declarative and breaks the declarative paradigm of object spread. |
| 80 | +- It makes the JS engine do extra work. First, the object spread will copy all the properties from all objects, and then we have to manually remove some of them, consuming additional CPU cycles, allocating the memory, and potentially, making the garbage collector care about a few more objects. |
| 81 | + |
| 82 | +What if there was a way to give developers more declarative superpowers here? |
33 | 83 |
|
34 | | -```js |
35 | | - b: Object.keys(src.b).reduce((acc, key) => key === 'b1' ? acc : { ...acc, [key]: src.b[key] }, {}), |
36 | | -```` |
37 | 84 |
|
| 85 | +## Proposed solution |
38 | 86 |
|
39 | | -## Proposal: the minus operator |
| 87 | +### The key exclusion syntax |
40 | 88 |
|
41 | | -What I propose is to improve readability of that quite typical operation, by introducing a special operator, expressed by a minus sign `-`, into the language standard. This operator would allow omitting a key by prependng it with this operator, in the object spread body: |
| 89 | +So, what if we could tell the JS engine not to copy some of the keys to the spread result at all? I am glad to present to you the key exclusion syntax, also mentioned as the minus syntax below. |
| 90 | + |
| 91 | +Looking into the aforementioned examples, all the problems would be solved elegantly: |
42 | 92 |
|
43 | 93 | ```js |
44 | | -const removeB1 = (src) => ({ |
45 | | - ...src, |
46 | | - b: { |
47 | | - ...src.b, |
48 | | - -b1, // A minus operator! |
49 | | - }, |
50 | | -}); |
| 94 | +// When the key name is known statically |
| 95 | +const sanitizedOpts = (opts) => { |
| 96 | + return { |
| 97 | + ...PRIVATE_OPTS, |
| 98 | + ..opts, |
| 99 | + -keyThatMustNotBeThere, // The key exclusion syntax in action! |
| 100 | + }; |
| 101 | +}; |
| 102 | + |
| 103 | +// When there are multiple keys with known names |
| 104 | +const sanitizedOpts = (opts) => { |
| 105 | + return { |
| 106 | + ...PRIVATE_OPTS, |
| 107 | + ..opts, |
| 108 | + -keyThatMustNotBeThere, |
| 109 | + -keyThatAlsoMustNotBeThere, // ...supporting multiple keys! |
| 110 | + }; |
| 111 | +}; |
| 112 | + |
| 113 | +// When the key name is not known beforehand |
| 114 | +const sanitizedOpts = (opts) => { |
| 115 | + return { |
| 116 | + ...PRIVATE_OPTS, |
| 117 | + ..opts, |
| 118 | + -[KEY_THAT_MUST_NOT_BE_THERE], // ... and dynamic keys! |
| 119 | + }; |
| 120 | +}; |
| 121 | + |
| 122 | +// When there are multiple keys to remove |
| 123 | +const sanitizedOpts = (opts) => { |
| 124 | + return { |
| 125 | + ...PRIVATE_OPTS, |
| 126 | + ..opts, |
| 127 | + -[...KEYS_TO_REMOVE], // ... and multiple dynamic keys! |
| 128 | + }; |
| 129 | +}; |
51 | 130 | ``` |
52 | 131 |
|
53 | | -I propose using minus (`-`) sign because it doesn't conflict with existing language semantics, and thus is easier to implement. |
| 132 | +#### Why the dash/the minus character? |
| 133 | + |
| 134 | +The assumption that it would be easier for the JS engines and transpilers to implement (because currently, the dash character is not expected before the key names in the object spread and thus won't conflict with any existing valid syntax). |
54 | 135 |
|
55 | 136 |
|
56 | 137 | ### Execution order |
|
0 commit comments