aleen42 / proposal-object-pick-or-omit

Introduce `Object.{pick,omit}` for operating properties of objects more conveniently.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 MouseEvent we are interested on 'ctrlKey', 'shiftKey', 'altKey', 'metaKey' events only.
  • We have a configObject and 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.body we want to extract ['name', 'company', 'email', 'password']
  • Checking if a component shouldReload by extracting compareKeys from props and compare it with prevProps.
  • Say we have a depsObject and we need to ignore all @internal/packages from it.
  • We have props from which we need to remove [‘_csrf’, ‘_method’]
  • We need to construct a newModelData by removing action.deleted from ({ ...state.models, ...action.update })
  • Filtering configuration objects when the filter list is given by a CLI argument.

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 for omit with dynamic values.
  • Destructuring cannot clone a new object while Object.pick can
  • Destructuring cannot pick up properties from the prototype while Object.pick can
  • Destructuring cannot pick properties dynamically, while Object.pick can
  • Destructuring cannot omit some properties, and we can only clone and delete without this proposal

We can read more about such use-cases and challenges from es.discourse below:

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 predict 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 the currentValue in the object.
    • object: the object pick was called upon.
  • thisArg (optional): the object used as this inside 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): Array.prototype[Symbol.iterator]}
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

  1. 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, 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. It has been reviewed in #6 which confirmed that the .{...} statement should denote what the new object should contain like restructuring:

      ({a : 1, b : 2, c : 3}).{a, d = 2}; // => {a: 1, d: 2}
      ({a : 1, b : 2, c : 3, d : 4}).{a, d = 2}; // => {a: 1, d: 4} 

    Nevertheless, it is just a simple vision, and feel free to discuss it.

FAQ

  1. When it comes to the prototype chain of an object, should the method pick or omit it?

    A: Consistent with destructuring: we can explicitly pick off properties of the prototype, but we can't modify them by calling Object.omit.

    Object.pick([1, 2, 3], ['length']); // => {length: 3}
    // equivalent to the behavior
    const {length} = [1, 2, 3];
    length; // => 3
    
    // cannot omit the prototype of an array by calling `Object.omit`
    const arr = [1, 2, 3];
    Object.omit(arr, ['length']);
    arr.length; // => 3

    The implementation of _.pick and _.omit by Lodash has also taken care about the chain.

    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.prototype}
    // equivalent to the behavior
    const {__proto__} = {};
    __proto__; // => Object.prototype
    
    Object.omit({}, ['__proto__']).__proto__; // => Object.prototype
    // equivalent to the behavior
    const {__proto__, ...res} = {}
    res.__proto__; // => Object.prototype

    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)));
  2. What is the type of returned value?

    A: Consistent with destructuring: should return plain objects.

    Object.pick([]); // => {}
    Object.omit([]); // => {}
    Object.pick(new Map()); // => {}
    Object.omit(new Map()); // => {}
    
    // equivalent to the behavior
    const {...res} = [];
    res instanceof Array; // => false
    const {...res} = new Map();
    res instanceof Map; // => false
    const {...res} = new Set();
    res instanceof Set; // => false
  3. How to handle Symbol?

    A: Consistent with destructuring.

    Object.pick([], [Symbol.iterator]); // => {Symbol(Symbol.iterator): Array.prototype[Symbol.iterator]}, pick off from the prototype
    // equivalent to the behavior
    const {[Symbol.iterator] : iter} = [];
    iter; // => Array.prototype[Symbol.iterator]
    
    Object.omit([], [Symbol.iterator]); // => {}, plain objects
    // equivalent to the behavior
    const {[Symbol.iterator] : iter, ...res} = [];
    res instanceof Array; // => false
    
    const symbol = Symbol('key');
    Object.omit({a : 1, [symbol]: 2}, [symbol]); // => {a: 1}
    // equivalent to the behavior
    const {[symbol] : _, ...res} = {a : 1, [symbol]: 2};
    res; // => {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
    // equivalent to the behavior
    const {[symbol] : sym, ...res} = {};
    sym; // => 'test'
    res[symbol]; // => 'test'
  4. If some properties of an object are not accessible like throwing an error, can Object.pick or Object.omit operate such an object?

    A: Consistent with destructuring: throw the error wrapped by Object.pick or Object.omit.

    Object.pick(Object.defineProperty({}, 'key', {
       get() { throw new Error(); }
    }), ['key']);
    // equivalent to the behavior
    const o = Object.defineProperty({}, 'key', {
      get() { throw new Error('custom'); }
    });
    try { const {key} = o; } catch (e) {
       e.message // => 'custom'
    }

    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
    
  5. 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 key1 and key2:

    postData({[key1] : o[key1], [key2] : o[key2]});
    postData(Object.pick(o, [key1, key2]));
  6. Why can't be defined on the Object.prototype directly?

    A: As Object is especially fundamental, both of them will result in conflicts of properties of any other objects. In shorthand, if defined, any objects inherited from Object with pick or omit defined in its prototype would break. Also, objects that inherit from null would be left unable to use this functionality.

  7. Why not define filtered methods corresponding to two actions: pickBy and omitBy like Lodash?

    A: It is unnecessary to double two methods because they can be combined into the argument instead:

    Besides, the passing filtered method can be easily reversed with equal meaning, and it means that omitBy can be easily defined as pickBy'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.

About

Introduce `Object.{pick,omit}` for operating properties of objects more conveniently.

License:MIT License


Languages

Language:HTML 100.0%