Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deep functionality for omit #723

Closed
ssesfahani opened this issue Sep 25, 2014 · 41 comments
Closed

Deep functionality for omit #723

ssesfahani opened this issue Sep 25, 2014 · 41 comments

Comments

@ssesfahani
Copy link

I was wondering if there could be an enhancement to support deep functionality for _.omit or if it has been discussed before?

@jdalton
Copy link
Member

jdalton commented Sep 25, 2014

We don't support deep omitting but can do deep partial matches of properties to omit because _.omit supports "_.where" callback shorthand, ex: _.omitBy(object, { 'a': { 'b': 2} }).

@megawac
Copy link
Contributor

megawac commented Sep 25, 2014

For pick/omit filtering arbitrary depth levels see this deepPick answer @ssesfahani

@aronwoost
Copy link

var o = {
  id: 1,
  value: "test",
  child: {
    id: 2,
    childValue: "test"
  }
}
_.omit(o, "value", "child.childValue");

... would be super awesome.

@bbottema
Copy link

bbottema commented Mar 8, 2016

We don't support deep omitting

Why is that, @jdalton, it's surely useful.

@jdalton
Copy link
Member

jdalton commented Mar 8, 2016

Why is that, it's surely useful.

Nothing against it. Just waiting for a bit more user demand for the feature.

@PizzaBrandon
Copy link

I needed something like this, too, so I came up with the following:

function omitDeep(collection, excludeKeys) {

  function omitFn(value) {

    if (value && typeof value === 'object') {
      excludeKeys.forEach((key) => {
        delete value[key];
      });
    }
  }

  return _.cloneDeepWith(collection, omitFn);
}

It can certainly be made to be more generic and I'm sure there are optimizations that could be done here, but it worked for me and got rid of my annoying "Maximum call stack size exceeded" error when doing the same thing recursively myself.

@vladimirbuskin
Copy link

vladimirbuskin commented Apr 18, 2016

i've searched for that, vote from me
+1

@jdalton
Copy link
Member

jdalton commented Apr 18, 2016

Don't forget to vote with reactions as it helps us sort by popularity.

This was referenced Jul 22, 2016
@rebers
Copy link

rebers commented Jul 25, 2016

Would love for both, _.omit and _.pick to work with paths.

@renehamburger
Copy link

BTW, this one is the most popular issue by now... Would it be worth putting it onto the roadmap?

@mohithg
Copy link

mohithg commented Nov 1, 2016

surely a good feature to be

@iam-peekay
Copy link

iam-peekay commented Nov 5, 2016

agreed! would be awesome and incredibly useful

@jdalton
Copy link
Member

jdalton commented Nov 5, 2016

Thanks for the show of support for this feature!
This request is now open to PRs if anyone wants to give it a try.

@avivr
Copy link
Contributor

avivr commented Nov 5, 2016

I would love to give it a try

@jdalton
Copy link
Member

jdalton commented Nov 5, 2016

@avivr The pull request contributing guide is a good place to start.

@doug-numetric
Copy link

Any chance for an array syntax for deep omit?

var o = {
  id: 1,
  value: "test",
  child: {
    id: 2,
    childValue: "test"
  }
}
// current way to handle computed key names
// let cv="childValue";
// _.omit(o, `child.${cv}`)
_.omit(o, ["child", "childValue"]);

For same reasons get supports this syntax.

@jdalton
Copy link
Member

jdalton commented Sep 26, 2017

@doug-numetric

Any chance for an array syntax for deep omit?

The omit methods aren't being carried over to Lodash v5.

@Billy-
Copy link

Billy- commented Oct 31, 2017

@jdalton how come? Is there a replacement?

@jdalton
Copy link
Member

jdalton commented Oct 31, 2017

Omit requires grabbing all possible properties (inherited, own, non-enumerable, symbols, etc.) and then filtering out properties. This is overly complicated and gets worse with deep functionality. It's better to pick the properties you want or simply delete the rogue ones.

@antony
Copy link

antony commented Oct 31, 2017

Lodash giveth, lodash taketh away :)

@ZephD
Copy link

ZephD commented Oct 31, 2017

Lodash giveth, lodash omit.

FTFY.

@bbottema
Copy link

bbottema commented Oct 31, 2017

Lodash giveth, lodash omit.

(but not deep)

@loopmode
Copy link

loopmode commented Apr 8, 2018

In my case, hard-source-webpack-plugin creates a hash of my webpack config, which is a fairly complex object, to check whether it can reuse previous results.
In production builds, certain properties of the config object contain dynamic values, like a timestamp in an output path, so the hash of the config object is always a new one. This effectively defeats the caching mechanism.
In such a case, picking/whitelisting is almost impossible whereas omitting/blacklisting a few well-known keys would be perfect.

I guess I'll have to clone the object, then somehow delete the rogue keys as mentioned above, then get it's hash.

@clodal
Copy link

clodal commented Dec 1, 2018

Recursively omit keys from an object by defining an array of keys to exclude in ES6 + Typescript.

omitDeep(myObject, [omitKey1, omitKey2, ...omitKeyN])

// omitDeep.ts

/**
 * Recursively remove keys from an object
 * @usage
 *
 * const input = {
 *   id: 1,
 *   __typename: '123',
 *   createdAt: '1020209',
 *   address: {
 *     id: 1,
 *     __typename: '123',
 *   },
 *   variants: [
 *     20,
 *     {
 *       id: 22,
 *       title: 'hello world',
 *       __typename: '123',
 *       createdAt: '1020209',
 *       variantOption: {
 *         id: 1,
 *         __typename: '123',
 *       },
 *     },
 *     {
 *       id: 32,
 *       __typename: '123',
 *       createdAt: '1020209',
 *     },
 *   ],
 * }
 *
 * const output = {
 *   id: 1,
 *   address: {
 *     id: 1,
 *   },
 *   variants: [
 *     20,
 *     {
 *       id: 22,
 *       title: 'hello world',
 *       variantOption: {
 *         id: 1,
 *       },
 *     },
 *     {
 *       id: 32,
 *     },
 *   ],
 * }
 *
 * expect(omitDeep(input, ['createdAt, 'updatedAt', __typename']).to.deep.equal(output) // true
 *
 * @param {object} input
 * @param {Array<number | string>>} excludes
 * @return {object}
 */
const omitDeep = (input: object, excludes: Array<number | string>): object => {
  return Object.entries(input).reduce((nextInput, [key, value]) => {
    const shouldExclude = excludes.includes(key)
    if (shouldExclude) return nextInput

    if (Array.isArray(value)) {
      const arrValue = value
      const nextValue = arrValue.map((arrItem) => {
        if (typeof arrItem === 'object') {
          return omitDeep(arrItem, excludes)
        }
        return arrItem
      })
      nextInput[key] = nextValue
      return nextInput
    } else if (typeof value === 'object') {
      nextInput[key] = omitDeep(value, excludes)
      return nextInput
    }

    nextInput[key] = value

    return nextInput
  }, {})
}

export default omitDeep

@Billy-
Copy link

Billy- commented Jan 11, 2019

FWIW: here's my version

@mxmzb
Copy link

mxmzb commented Jan 22, 2019

_.omit(o, "value", "child.childValue"); works right now, so no need to do it yourself ;)

@Billy-
Copy link

Billy- commented Jan 24, 2019

@mxmzb the main use-case that isn't supported is arrays (iterate over all items in an array)

@mxmzb
Copy link

mxmzb commented Jan 24, 2019

@Billy- If you have explicit values and care about data structure, which I believe you should do, this is a very elegant approach:

const myNewObjectWithOmittedParams = Object.assign(
  {},
  {
    ...omit(myOldObject, ["bar", "foo.bar"])
  },
  {
    someChildArrayProp: myOldObject.someChildArrayProp.map(obj => omit(obj, ["bar", "foo.bar"]))
  }
}

@bdelmas
Copy link

bdelmas commented Apr 17, 2019

@mxmzb Unless I have the wrong version but _.omit(o, "value", "child.childValue"); doesn't work for "child.childValue".

Edit:

It doensn't work with the lastest "lodash.omit": "^4.5.0",
But it does work with the full package "lodash": "^4.17.11",

lodash.omit has not being updated for too long.

@rwoody
Copy link

rwoody commented Oct 3, 2019

@clodal That works (I also had the same use case with __typename). I tweaked it a little bit to safely handle null/undefined values

@blikblum
Copy link
Contributor

blikblum commented Oct 3, 2019

I tweaked it a little bit to safely handle null/undefined values

Can you post your version?

@dgsunesen
Copy link

@PizzaBrandon - Awesomesauce solution!

@jeshio
Copy link

jeshio commented Aug 20, 2020

@clodal thank you
This is with null fields handle version:

const omitDeep = (input: object, excludes: Array<number | string>): object => {
  return Object.entries(input).reduce((nextInput, [key, value]) => {
    const shouldExclude = excludes.includes(key)
    if (shouldExclude) return nextInput

    if (Array.isArray(value)) {
      const arrValue = value
      const nextValue = arrValue.map((arrItem) => {
        if (typeof arrItem === 'object' && arrItem !== null) {
          return omitDeep(arrItem, excludes)
        }
        return arrItem
      })
      nextInput[key] = nextValue
      return nextInput
    } else if (typeof value === 'object' && arrItem !== null) {
      nextInput[key] = omitDeep(value, excludes)
      return nextInput
    }

    nextInput[key] = value

    return nextInput
  }, {})
}

export default omitDeep

@cSweetMaj7
Copy link

@jeshio This is an NPE

} else if (typeof value === 'object' && arrItem !== null) {

arrItem only exists in the scope of the above arrValue.map.

@alfasin
Copy link

alfasin commented Nov 16, 2020

Why is that, it's surely useful.

Nothing against it. Just waiting for a bit more user demand for the feature.

Per all the responses here, it looks like there's a demand IMO.
Can we reopen?

@ulisesbocchio
Copy link

you can use cloneDeepWith:

function omitDeep(input, excludes) {
    const omit = [];
    const clone = _.cloneDeepWith(input, (value, key, object, stack) => {
        if(!_.isUndefined(key) && excludes.includes(key)) {
            omit.push({ key, from: stack.get(object) });
            return null;
        }
    });
    omit.forEach(({ key, from }) => _.unset(from, key));
    return clone;
}

I've been using something a bit more involved to omit anything past a given depth or have exclusion rules be regexes on full paths from the root of the input:

function deepCustomizer(customizer) {
    const depthMap = new WeakMap();
    return (value, key, object, stack) => {
        let depth = 0;
        let path = [];
        if (!_.isUndefined(key)) {
            const { depth: parentDepth, path: parentPath } = depthMap.get(object);
            depth = parentDepth + 1;
            path = [...parentPath, key];
        }
        if (_.isObject(value) && !_.isBuffer(value)) {
            depthMap.set(value, { depth, path });
        }
        return customizer(value, key, object, stack, depth, path);
    };
}

function omitDeep(input, excludes, maxDepth = Infinity) {
    const omit = [];
    const isExcluded = pathString => _.some(excludes, exclude => _.isRegExp(exclude) ? exclude.test(pathString) : exclude === pathString);
    const clone = _.cloneDeepWith(input, deepCustomizer((value, key, object, stack, depth, path) => {
        const pathString = _.join(path, '.');
        if(depth > maxDepth || isExcluded(pathString)) {
            omit.push({ key, from: stack.get(object) });
            return null;
        }
    }));
    omit.forEach(({ key, from }) => _.unset(from, key));
    return clone;
}

but it would be nice if at least lodash's customizer provides depth/path and a way to instruct omissions, maybe with a Symbol.

@lveillard
Copy link

Recursively omit keys from an object by defining an array of keys to exclude in ES6 + Typescript.

omitDeep(myObject, [omitKey1, omitKey2, ...omitKeyN])

// omitDeep.ts

/**
 * Recursively remove keys from an object
 * @usage
 *
 * const input = {
 *   id: 1,
 *   __typename: '123',
 *   createdAt: '1020209',
 *   address: {
 *     id: 1,
 *     __typename: '123',
 *   },
 *   variants: [
 *     20,
 *     {
 *       id: 22,
 *       title: 'hello world',
 *       __typename: '123',
 *       createdAt: '1020209',
 *       variantOption: {
 *         id: 1,
 *         __typename: '123',
 *       },
 *     },
 *     {
 *       id: 32,
 *       __typename: '123',
 *       createdAt: '1020209',
 *     },
 *   ],
 * }
 *
 * const output = {
 *   id: 1,
 *   address: {
 *     id: 1,
 *   },
 *   variants: [
 *     20,
 *     {
 *       id: 22,
 *       title: 'hello world',
 *       variantOption: {
 *         id: 1,
 *       },
 *     },
 *     {
 *       id: 32,
 *     },
 *   ],
 * }
 *
 * expect(omitDeep(input, ['createdAt, 'updatedAt', __typename']).to.deep.equal(output) // true
 *
 * @param {object} input
 * @param {Array<number | string>>} excludes
 * @return {object}
 */
const omitDeep = (input: object, excludes: Array<number | string>): object => {
  return Object.entries(input).reduce((nextInput, [key, value]) => {
    const shouldExclude = excludes.includes(key)
    if (shouldExclude) return nextInput

    if (Array.isArray(value)) {
      const arrValue = value
      const nextValue = arrValue.map((arrItem) => {
        if (typeof arrItem === 'object') {
          return omitDeep(arrItem, excludes)
        }
        return arrItem
      })
      nextInput[key] = nextValue
      return nextInput
    } else if (typeof value === 'object') {
      nextInput[key] = omitDeep(value, excludes)
      return nextInput
    }

    nextInput[key] = value

    return nextInput
  }, {})
}

export default omitDeep

This one breaks when having null values, you can filter them like this:

[...]
return Object.entries(input)
    .filter((x) => x[1] !== null) /newline
    .reduce [...]

@lsaadeh
Copy link

lsaadeh commented May 19, 2022

For my use case I just needed to remove undefined variables.. here my attempt.

const omitDeep = (input: any, omitValue = undefined): any => {
    if (Array.isArray(input)) {
      const arrValue = input.filter((e) => e !== omitValue);
      const nextValue = arrValue.map((arrItem) => {
        return this.omitDeep(arrItem, omitValue);
      });
      return nextValue;
    } else if (typeof input === 'object') {
      return Object.entries(input).reduce((nextInput: any, [key, value]) => {
        const shouldExclude = value === omitValue;
        if (shouldExclude) {
          return nextInput;
        }
        nextInput[key] = this.omitDeep(value, omitValue);
        return nextInput;
      }, {});
    } else {
      return input;
    }
  }
const object = {
    a: '',
    b: undefined,
  };

  const input = {
    a: '',
    b: undefined,
    c: [
      {
        a: '',
        b: undefined,
      },
      [
        {
          a: '',
          b: undefined,
        },
        [
          {
            a: '',
            b: undefined,
          },
          object,
          ['a', 'b', 'c', undefined, 1, 2, 3, ['a', 'b', 'c', undefined, 1, 2, 3, ['a', 'b', 'c', undefined, 1, 2, 3]]],
        ],
      ],
    ],
    d: {
      a: 'abc',
      b: undefined,
      c: {
        a: undefined,
        b: '',
      },
      d: 123,
    },
    e: object,
    f: ['a', 'b', 'c', undefined, 1, 2, 3, ['a', 'b', 'c', undefined, 1, 2, 3, ['a', 'b', 'c', undefined, 1, 2, 3]]],
  };

  const output = {
    a: '',
    c: [
      {
        a: '',
      },
      [
        {
          a: '',
        },
        [
          {
            a: '',
          },
          {
            a: '',
          },
          ['a', 'b', 'c', 1, 2, 3, ['a', 'b', 'c', 1, 2, 3, ['a', 'b', 'c', 1, 2, 3]]],
        ],
      ],
    ],
    d: {
      a: 'abc',
      c: {
        b: '',
      },
      d: 123,
    },
    e: {
      a: '',
    },
    f: ['a', 'b', 'c', 1, 2, 3, ['a', 'b', 'c', 1, 2, 3, ['a', 'b', 'c', 1, 2, 3]]],
  };

  [
    {
      description: 'should remove all values specified by omitDeep in an object',
      input: input,
      output: output,
    },
    {
      description: 'should remove all values specified by omitDeep in an array',
      input: [input],
      output: [output],
    },
  ].forEach((test) => {
    it(test.description, async () => {
      const json = omitDeep(test.input);
      expect(json).to.eql(test.output);
    });
  });

@teja123r
Copy link

teja123r commented Jun 8, 2022

I just had to deep omit certain keys given an object that might contain Arrays. Here's my attempt

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const omitDeep = (obj: any, omitKeys: string[]): any => {
  if (Array.isArray(obj)) {
    return obj.map((v) => omitDeep(v, omitKeys));
  } else if (obj != null && obj.constructor === Object) {
    for (const key in obj) {
      const value = obj[key];
      if (value != null && value.constructor === Object) {
        omitDeep(value, omitKeys);
      } else if (omitKeys.includes(key)) {
        delete obj[key];
      }
    }
  }
  return obj;
};

@esistgut
Copy link

Typescript version of #723 (comment):

export function omitDeep<T>(collection: T, excludeKeys: string[]): T {
  function omitFn(value: any) {
    if (value && typeof value === 'object') {
      excludeKeys.forEach((key) => {
        delete value[key];
      });
    }
  }

  return cloneDeepWith(collection, omitFn);
}

@azerum
Copy link

azerum commented Jun 10, 2023

Here's a TypeScript types-aware version (doesn't work with Map and Set, but those can be added):

export type OmitDeep<T, K extends string> = 
  T extends (infer U)[]
    ? OmitDeep<U, K>[]
    : T extends Record<any, any>
      ? { [P in keyof T as P extends K ? never : P]: OmitDeep<T[P], K> }
      : T;

export function omitDeep<
  T extends object,
  K extends string,
>(obj: T, key: K): OmitDeep<T, K> {
  const clone: any = {};

  for (const property in obj) {
    const value = obj[property];

    //@ts-expect-error The types of those values can overlap
    if (property === key) {
      continue;
    }

    if (Array.isArray(value)) {
      clone[property] = value.map((item) => omitDeep(item, key));
      continue;
    }

    if (value !== null && typeof value === "object") {
      clone[property] = omitDeep(value, key);
      continue;
    }

    clone[property] = value;
  }

  return clone;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests