Stage 1
Classification: API Change
Human Validated: KW
Title: Object pick/omit
Authors: Hemanth HM, Aleen
Champions: Hemanth HM
Last Presented: July 2022
Stage Upgrades:
Stage 1: 2022-07-22
Stage 2: NA
Stage 2.7: NA
Stage 3: NA
Stage 4: NA
Last Commit: 2022-08-05
Topics: others objects
Keywords: pick omit static getter
GitHub Link: https://github.com/tc39/proposal-object-pick-or-omit
GitHub Note Link: https://github.com/tc39/notes/blob/HEAD/meetings/2022-07/jul-21.md#ergonomic-dynamic-object-restructuring
Proposal Description:
Object.{pick, omit}
ECMAScript Proposal, specs, and reference implementation for
Object.pick,Object.omit.
Authors: @Aleen && Hemanth HM
Champion: @js-choi
This proposal is currently stage 1 of the process.
Motivation
Let us consider a few scenarios from the real world to understand what we are trying to solve in this proposal.
- On
MouseEventwe are interested on'ctrlKey', 'shiftKey', 'altKey', 'metaKey'events only. - We have a
configObjectand we need['dependencies', 'devDependencies', 'peerDependencies']from it. - We have an
optionsBagand we would allow on['shell', 'env', 'extendEnv', 'uid', 'gid']on it. - From a
req.bodywe want to extract['name', 'company', 'email', 'password'] - Checking if a component
shouldReloadby extractingcompareKeysfrompropsand compare it withprevProps. - Say we have a
depsObjectand we need to ignore all@internal/packagesfrom it. - We have
propsfrom which we need to remove[‘_csrf’, ‘_method’] - We need to construct a
newModelDataby removingaction.deletedfrom({ ...state.models, ...action.update }) - Filtering configuration objects when the filter list is given by a
CLIargument.
Well, you see life is all about picking what we want and omiting what we don’t!
Would life be easier if the language provided a convenient method to help us during similar scenarios?
Now, one might argue saying we can implement pick and omit as below:
const pick = (obj, keys) => Object.fromEntries(
keys.map(k => obj.hasOwnProperty(k) && [k, obj[k]]).filter(x => x)
);
/*
We can also use a Destructuring assignment
const { authKey, ...toLog } = userInfo;
*/const omit = (obj, keys) => Object.fromEntries(
keys.map(k => !obj.hasOwnProperty(k) && [k, obj[k]]).filter(x => x)
);The major challenges we see with the above implementations:
- It is not ergonomic!
- If we opt for the destructuring way it doesn’t work at all for
pick, or foromitwith dynamic values. - Destructuring cannot
clonea new object whileObject.pickcan - Destructuring cannot
pickup properties from theprototypewhileObject.pickcan - Destructuring cannot
pickproperties dynamically, whileObject.pickcan - Destructuring cannot
omitsome properties, and we can onlycloneanddeletewithout this proposal
We can read more about such use-cases and challenges from es.discourse below:
- Object.{pick,omit}.
- Object restructuring syntax
- Object Array Pick
- js-pick-notation
- slect multiple object values
With that in mind would it not be easier if we had Object.pick and Object.omit static methods?!
Let us now discuss what the API of such a helpful method would be?
Syntax
Object.pick(obj[, pickedKeys | predictedFunction(currentValue[, key[, object]])[, thisArg])
Object.omit(obj[, omittedKeys | predictedFunction(currentValue[, key[, object]])[, thisArg])
Parameters
obj: which object you want to pick or omit.pickedKeys(optional): keys of properties you want to pick from the object. The default value is an empty array.omittedKeys(optional): keys of properties you want to pick from the object. The default value is an empty array.predictedFunction(optional): the function to predicted whether the property should be picked or omitted. The default value is an identity:x => x.currentValue: the current value processed in the object.key: the key of thecurrentValuein the object.object: the objectpickwas called upon.
thisArg(optional): the object used asthisinside the predicted function.
Returns
- Returns a new object, which has picked or omitted properties from the object.
Usage
// default
Object.pick({a : 1}); // => {}
Object.omit({a : 1}); // => {a: 1}
Object.pick({a : 0, b : 1}, v => v); // => {b: 1}
Object.pick({a : 0, b : 1}, v => !v); // => {a: 0}
Object.pick({}, function () { console.log(this) }); // => the object itself
Object.pick({}, function () { console.log(this) }, window); // => Window
Object.pick({a : 1, b : 2}, ['a']); // => {a: 1}
Object.omit({a : 1, b : 2}, ['b']); // => {a: 1}
Object.pick({a : 1, b : 2}, ['c']); // => {}
Object.omit({a : 1, b : 2}, ['c']); // => {a: 1, b: 2}
Object.pick([], [Symbol.iterator]); // => {Symbol(Symbol.iterator): f}
Object.pick([], ['length']); // => {length: 0}
Object.pick({a : 1, b : 2}, v => v === 1); // => {a: 1}
Object.pick({a : 1, b : 2}, v => v !== 2); // => {a: 1}
Object.pick({a : 1, b : 2}, (v, k) => k === 'a'); // => {a: 1}
Object.pick({a : 1, b : 2}, (v, k) => k !== 'b'); // => {a: 1}Visions
-
A syntax sugar in the case of picking:
To extend the motivation of this proposal, there may be some syntax notations as an alternative of picking properties from objects, like the proposal, slice-notation:
There are two ideas around how to wrap picking keys:
-
square brackets:
({a : 1, b : 2, c : 3}).['a', 'b']; // => {a : 1, b : 2} const keys = ['a', 'b']; ({a : 1, b : 2, c : 3}).[keys[0], keys[1]]; // => {a : 1, b : 2} ({a : 1, b : 2, c : 3}).[...keys]; // => {a : 1, b : 2} -
curly brackets
({a : 1, b : 2, c : 3}).{a, b} // => {a : 1, b : 2} const keys = ['a', 'b']; ({a : 1, b : 2, c : 3}).{[keys[0]], b}; // => {a : 1} ({a : 1, b : 2, c : 3}).{[...keys]}; // => {a : 1, b : 2} // Similar to destructuring ({a : 1, b : 2, c : 3}).{a, b : B}; // => {a : 1, B : 2}Currently, there is a disagreement on whether properties with default assignment values should be picked.
// If considering the meaning of picking, the initial value has no meanings ({a : 1, b : 2, c : 3}).{a, d = 2}; // => {a : 1} // If considering as "restructuring", the shortcut has its reason to pick ({a : 1, b : 2, c : 3}).{a, d = 2}; // => {a : 1, d : 2}
Nevertheless, it is just a simple vision, and feel free to discuss.
-
FAQ
-
When it comes to the prototype chain of an object, should the method pick or omit it? (The answer may change)
A: The implementation of
_.pickand_.omitby Lodash has taken care about the chain. To keep the rule, we can pick off properties of prototype, but can’t omit them:Object.pick({a : 1}, ['toString']); // => {toString: f} Object.omit({a : 1}, ['toString']).toString; // => ƒ toString() { [native code] }The same rule applies to
__proto__event if it has been deprecated, because the proposal should be pure enough to not specify a special logic to eliminate deprecated properties:Object.pick({}, ['__proto__']); // => {__proto__: {...}} Object.omit({}, ['__proto__']).__proto__; // => {...}In some opinions, picking off or omitting properties from the prototype chain should make the method more extendable:
const pickOwn = (obj, keys) => Object.pick(obj, keys.filter(key => obj.hasOwnProperty(key))); const omitOwn = (obj, keys) => Object.omit(obj, keys.filter(key => obj.hasOwnProperty(key))); -
What is the type of the returned value?
A: All these methods should return plain objects:
Object.pick([]); // => {} Object.omit([]); // => {} Object.pick(new Map()); // => {} Object.omit(new Map()); // => {} -
How to handle
Symbol?A:
Symbolshould just be considered as properties withinSymbolkeys, and they should obey the rules mentioned above:Object.pick([], [Symbol.iterator]); // => {Symbol(Symbol.iterator): f}, pick off from the prototype Object.omit([], [Symbol.iterator]); // => {}, plain objects const symbol = Symbol('key'); Object.omit({a : 1, [symbol]: 2}, [symbol]); // => {a : 1} Object.prototype[symbol] = 'test'; // override prototype Object.pick({}, [symbol]); // => {Symbol(key): "test"}, pick off from the prototype Object.omit({}, [symbol])[symbol]; // => "test", cannot omit properties from the prototype -
If some properties of an object are not accessible like throwing an error, can
Object.pickorObject.omitoperate such an object?A: I suggest throwing the error wrapped by
Object.pickorObject.omit, but it is NOT the final choice:Object.pick(Object.defineProperty({}, 'key', { get() { throw new Error() } }), ['key']);The error stack will look like this:
Uncaught Error at Object.get (<anonymous>:2:20) at Object.pick (<anonymous>:2:10) at <anonymous>:1:8 -
In comparison with proposal-shorthand-improvements, when should we use these two methods?
A: Multiple properties. Assume that we need to ensure an object without any side-effected keys except
key1andkey2:postData({[key1] : o[key1], [key2] : o[key2]}); postData(Object.pick(o, [key1, key2])); -
Why can’t be defined on the
Object.prototypedirectly?A: As
Objectis especially fundamental, and both of them will result in conflicts of properties of any other objects. In shorthand, if defined, any objects inherited fromObjectwithpickoromitdefined in its prototype should break. -
Why not define filtered methods corresponding to two actions:
pickByandomitBylike Lodash?A: It is unnecessary to double two methods, because it can be combined into the argument instead:
Besides, the passing filtered method can be easily reversed with equal meaning, and it means that
omitBycan be easily defined aspickBy’s inverse.Object.pick({a : 1, b : 2}, v => v); // Equivalent to the following: Object.omitBy({a: 1, b : 2}, v => !v);
Notice: If you have any suggestions or ideas about this proposal? Appreciate your discussions via issues.