From 2e759cb8b464472cda5dcccd1c08ff4031c951e0 Mon Sep 17 00:00:00 2001 From: kevaldonga Date: Sun, 5 Jul 2026 15:41:15 +0530 Subject: [PATCH 1/2] feat (object/omit-deep): add 'omitDeep' function to omit fields from deeply nested objects or arrays --- docs/ja/reference/object/omitDeep.md | 74 +++++++ docs/ko/reference/object/omitDeep.md | 74 +++++++ docs/reference/object/omitDeep.md | 74 +++++++ docs/zh_hans/reference/object/omitDeep.md | 74 +++++++ src/object/index.ts | 1 + src/object/omitDeep.spec.ts | 225 ++++++++++++++++++++++ src/object/omitDeep.ts | 117 +++++++++++ 7 files changed, 639 insertions(+) create mode 100644 docs/ja/reference/object/omitDeep.md create mode 100644 docs/ko/reference/object/omitDeep.md create mode 100644 docs/reference/object/omitDeep.md create mode 100644 docs/zh_hans/reference/object/omitDeep.md create mode 100644 src/object/omitDeep.spec.ts create mode 100644 src/object/omitDeep.ts diff --git a/docs/ja/reference/object/omitDeep.md b/docs/ja/reference/object/omitDeep.md new file mode 100644 index 000000000..4c2245def --- /dev/null +++ b/docs/ja/reference/object/omitDeep.md @@ -0,0 +1,74 @@ +# omitDeep + +指定されたネストされたパスを除外した新しいオブジェクトを返します。 + +```typescript +const result = omitDeep(object, paths); +``` + +## 使用法 + +### `omitDeep(object, paths)` + +オブジェクトから特定のネストされたプロパティを除外したい時に`omitDeep`を使用してください。ドット(`.`)で区切られたパスに対応するプロパティを削除した新しいオブジェクトを返します。ネストされたオブジェクトと配列内のオブジェクトも再帰的に処理されます。 + +```typescript +import { omitDeep } from 'es-toolkit/object'; + +// ネストされたプロパティを除外 +const obj = { a: 1, b: { x: 2, y: 3 }, c: 4 }; +const result = omitDeep(obj, ['b.x']); +// resultは{ a: 1, b: { y: 3 }, c: 4 }になります + +// 深くネストされたプロパティを除外 +const nested = { + user: { + id: 1, + profile: { + name: 'John', + email: 'john@example.com', + }, + }, +}; +const nestedResult = omitDeep(nested, ['user.profile.email']); +// nestedResultは{ +// user: { +// id: 1, +// profile: { +// name: 'John', +// }, +// }, +// }になります + +// 配列内のすべてのオブジェクトからプロパティを除外 +const users = { + users: [ + { id: 1, secret: 'abc' }, + { id: 2, secret: 'def' }, + ], +}; +const withoutSecrets = omitDeep(users, ['users.secret']); +// withoutSecretsは{ +// users: [ +// { id: 1 }, +// { id: 2 }, +// ], +// }になります + +// ネストされたオブジェクトや配列全体を除外 +const data = { + user: { id: 1, profile: { name: 'John' } }, + items: [1, 2, 3], +}; +const trimmed = omitDeep(data, ['user.profile', 'items']); +// trimmedは{ user: { id: 1 } }になります +``` + +#### パラメータ + +- `object` (`T`): パスを除外するオブジェクトです。 +- `paths` (`readonly string[]`): オブジェクトから除外するドット区切りのパスの配列です。 + +#### 戻り値 + +(`OmitDeep`): 指定されたパスが除外された新しいオブジェクトを返します。 diff --git a/docs/ko/reference/object/omitDeep.md b/docs/ko/reference/object/omitDeep.md new file mode 100644 index 000000000..5c0be401b --- /dev/null +++ b/docs/ko/reference/object/omitDeep.md @@ -0,0 +1,74 @@ +# omitDeep + +지정된 중첩 경로들을 제외한 새로운 객체를 반환해요. + +```typescript +const result = omitDeep(object, paths); +``` + +## 사용법 + +### `omitDeep(object, paths)` + +객체에서 특정 중첩 속성들을 제외하고 싶을 때 `omitDeep`을 사용하세요. 점(`.`)으로 구분된 경로에 해당하는 속성들을 제거한 새로운 객체를 반환해요. 중첩된 객체와 배열 내의 객체들도 재귀적으로 처리돼요. + +```typescript +import { omitDeep } from 'es-toolkit/object'; + +// 중첩된 속성을 제외해요 +const obj = { a: 1, b: { x: 2, y: 3 }, c: 4 }; +const result = omitDeep(obj, ['b.x']); +// result는 { a: 1, b: { y: 3 }, c: 4 }가 돼요 + +// 깊게 중첩된 속성을 제외해요 +const nested = { + user: { + id: 1, + profile: { + name: 'John', + email: 'john@example.com', + }, + }, +}; +const nestedResult = omitDeep(nested, ['user.profile.email']); +// nestedResult는 { +// user: { +// id: 1, +// profile: { +// name: 'John', +// }, +// }, +// }가 돼요 + +// 배열 내 모든 객체에서 특정 속성을 제외해요 +const users = { + users: [ + { id: 1, secret: 'abc' }, + { id: 2, secret: 'def' }, + ], +}; +const withoutSecrets = omitDeep(users, ['users.secret']); +// withoutSecrets는 { +// users: [ +// { id: 1 }, +// { id: 2 }, +// ], +// }가 돼요 + +// 중첩된 객체나 배열 전체를 제외해요 +const data = { + user: { id: 1, profile: { name: 'John' } }, + items: [1, 2, 3], +}; +const trimmed = omitDeep(data, ['user.profile', 'items']); +// trimmed는 { user: { id: 1 } }가 돼요 +``` + +#### 파라미터 + +- `object` (`T`): 경로를 제외할 객체예요. +- `paths` (`readonly string[]`): 객체에서 제외할 점으로 구분된 경로들의 배열이에요. + +#### 반환 값 + +(`OmitDeep`): 지정된 경로들이 제외된 새로운 객체를 반환해요. diff --git a/docs/reference/object/omitDeep.md b/docs/reference/object/omitDeep.md new file mode 100644 index 000000000..d1114defb --- /dev/null +++ b/docs/reference/object/omitDeep.md @@ -0,0 +1,74 @@ +# omitDeep + +Returns a new object excluding the specified nested paths. + +```typescript +const result = omitDeep(object, paths); +``` + +## Usage + +### `omitDeep(object, paths)` + +Use `omitDeep` when you want to exclude specific nested properties from an object. It returns a new object with the properties corresponding to the specified dot-separated paths removed. Nested objects and objects within arrays are processed recursively. + +```typescript +import { omitDeep } from 'es-toolkit/object'; + +// Omit a nested property +const obj = { a: 1, b: { x: 2, y: 3 }, c: 4 }; +const result = omitDeep(obj, ['b.x']); +// result is { a: 1, b: { y: 3 }, c: 4 } + +// Omit deeply nested properties +const nested = { + user: { + id: 1, + profile: { + name: 'John', + email: 'john@example.com', + }, + }, +}; +const nestedResult = omitDeep(nested, ['user.profile.email']); +// nestedResult is { +// user: { +// id: 1, +// profile: { +// name: 'John', +// }, +// }, +// } + +// Omit a property from every object in an array +const users = { + users: [ + { id: 1, secret: 'abc' }, + { id: 2, secret: 'def' }, + ], +}; +const withoutSecrets = omitDeep(users, ['users.secret']); +// withoutSecrets is { +// users: [ +// { id: 1 }, +// { id: 2 }, +// ], +// } + +// Omit an entire nested object or array +const data = { + user: { id: 1, profile: { name: 'John' } }, + items: [1, 2, 3], +}; +const trimmed = omitDeep(data, ['user.profile', 'items']); +// trimmed is { user: { id: 1 } } +``` + +#### Parameters + +- `object` (`T`): The object to exclude paths from. +- `paths` (`readonly string[]`): An array of dot-separated paths to exclude from the object. + +#### Returns + +(`OmitDeep`): A new object with the specified paths excluded. diff --git a/docs/zh_hans/reference/object/omitDeep.md b/docs/zh_hans/reference/object/omitDeep.md new file mode 100644 index 000000000..e0f938366 --- /dev/null +++ b/docs/zh_hans/reference/object/omitDeep.md @@ -0,0 +1,74 @@ +# omitDeep + +返回一个排除指定嵌套路径的新对象。 + +```typescript +const result = omitDeep(object, paths); +``` + +## 用法 + +### `omitDeep(object, paths)` + +当您想要从对象中排除特定的嵌套属性时,请使用 `omitDeep`。它返回一个新对象,其中删除了与指定点分隔路径对应的属性。嵌套对象和数组中的对象也会递归处理。 + +```typescript +import { omitDeep } from 'es-toolkit/object'; + +// 排除嵌套属性 +const obj = { a: 1, b: { x: 2, y: 3 }, c: 4 }; +const result = omitDeep(obj, ['b.x']); +// result 是 { a: 1, b: { y: 3 }, c: 4 } + +// 排除深层嵌套属性 +const nested = { + user: { + id: 1, + profile: { + name: 'John', + email: 'john@example.com', + }, + }, +}; +const nestedResult = omitDeep(nested, ['user.profile.email']); +// nestedResult 是 { +// user: { +// id: 1, +// profile: { +// name: 'John', +// }, +// }, +// } + +// 从数组中的每个对象排除属性 +const users = { + users: [ + { id: 1, secret: 'abc' }, + { id: 2, secret: 'def' }, + ], +}; +const withoutSecrets = omitDeep(users, ['users.secret']); +// withoutSecrets 是 { +// users: [ +// { id: 1 }, +// { id: 2 }, +// ], +// } + +// 排除整个嵌套对象或数组 +const data = { + user: { id: 1, profile: { name: 'John' } }, + items: [1, 2, 3], +}; +const trimmed = omitDeep(data, ['user.profile', 'items']); +// trimmed 是 { user: { id: 1 } } +``` + +#### 参数 + +- `object` (`T`):要排除路径的对象。 +- `paths` (`readonly string[]`):要从对象中排除的点分隔路径的数组。 + +#### 返回值 + +(`OmitDeep`):排除了指定路径的新对象。 diff --git a/src/object/index.ts b/src/object/index.ts index 1fbcd9a83..4b452948d 100644 --- a/src/object/index.ts +++ b/src/object/index.ts @@ -10,6 +10,7 @@ export { merge } from './merge.ts'; export { mergeWith } from './mergeWith.ts'; export { omit } from './omit.ts'; export { omitBy } from './omitBy.ts'; +export { omitDeep, type OmitDeep } from './omitDeep.ts'; export { pick } from './pick.ts'; export { pickBy } from './pickBy.ts'; export { sortKeys } from './sortKeys.ts'; diff --git a/src/object/omitDeep.spec.ts b/src/object/omitDeep.spec.ts new file mode 100644 index 000000000..e49ec4995 --- /dev/null +++ b/src/object/omitDeep.spec.ts @@ -0,0 +1,225 @@ +import { describe, expect, expectTypeOf, it } from 'vitest'; +import { omitDeep } from './omitDeep'; + +describe('omitDeep', () => { + it('should omit top-level properties from an object', () => { + const object = { foo: 1, bar: 2, baz: 3 }; + const result = omitDeep(object, ['foo', 'bar']); + expect(result).toEqual({ baz: 3 }); + }); + + it('should omit nested properties using dot-separated paths', () => { + const object = { a: 1, b: { x: 2, y: 3 }, c: 4 }; + const result = omitDeep(object, ['b.x']); + expect(result).toEqual({ a: 1, b: { y: 3 }, c: 4 }); + }); + + it('should omit deeply nested properties', () => { + const object = { + user: { + id: 1, + profile: { + name: 'John', + email: 'john@example.com', + }, + }, + }; + const result = omitDeep(object, ['user.profile.email']); + expect(result).toEqual({ + user: { + id: 1, + profile: { + name: 'John', + }, + }, + }); + }); + + it('should omit multiple paths at once', () => { + const object = { + a: 1, + b: { x: 2, y: 3 }, + c: { p: 4, q: 5 }, + }; + const result = omitDeep(object, ['b.x', 'c.q']); + expect(result).toEqual({ + a: 1, + b: { y: 3 }, + c: { p: 4 }, + }); + }); + + it('should omit an entire nested object', () => { + const object = { + a: 1, + b: { + x: 2, + y: 3, + }, + c: 4, + }; + const result = omitDeep(object, ['b']); + expect(result).toEqual({ a: 1, c: 4 }); + }); + + it('should omit an entire inner object while keeping siblings', () => { + const object = { + user: { + id: 1, + profile: { + name: 'John', + email: 'john@example.com', + }, + settings: { + theme: 'dark', + }, + }, + }; + const result = omitDeep(object, ['user.profile']); + expect(result).toEqual({ + user: { + id: 1, + settings: { + theme: 'dark', + }, + }, + }); + }); + + it('should omit a property from objects inside an array', () => { + const object = { + users: [ + { id: 1, secret: 'abc' }, + { id: 2, secret: 'def' }, + ], + }; + const result = omitDeep(object, ['users.secret']); + expect(result).toEqual({ + users: [{ id: 1 }, { id: 2 }], + }); + }); + + it('should omit an entire array field', () => { + const object = { + users: [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + ], + total: 2, + }; + const result = omitDeep(object, ['users']); + expect(result).toEqual({ total: 2 }); + }); + + it('should omit properties from objects in nested arrays', () => { + const object = { + items: [[{ x: 1, y: 2 }], [{ x: 3, y: 4 }]], + }; + const result = omitDeep(object, ['items.y']); + expect(result).toEqual({ + items: [[{ x: 1 }], [{ x: 3 }]], + }); + }); + + it('should return an empty object if all keys are omitted', () => { + const object = { a: 1, b: 2, c: 3 }; + const result = omitDeep(object, ['a', 'b', 'c']); + expect(result).toEqual({}); + }); + + it('should return the same structure if no paths are omitted', () => { + const object = { a: 1, b: { x: 2, y: 3 }, c: 4 }; + const result = omitDeep(object, []); + expect(result).toEqual({ a: 1, b: { x: 2, y: 3 }, c: 4 }); + }); + + it('should not affect the original object', () => { + const object = { a: 1, b: { x: 2, y: 3 }, c: 4 }; + const result = omitDeep(object, ['b.x']); + expect(result).toEqual({ a: 1, b: { y: 3 }, c: 4 }); + expect(object).toEqual({ a: 1, b: { x: 2, y: 3 }, c: 4 }); + }); + + it('should handle empty objects and arrays', () => { + expect(omitDeep({}, [])).toEqual({}); + expect(omitDeep({ a: {} }, [])).toEqual({ a: {} }); + expect(omitDeep({ a: [] }, [])).toEqual({ a: [] }); + + const emptyObject = { a: {} }; + const emptyArray = { a: [] as [] }; + + // Runtime: omitting a non-existent nested path is a no-op. + // TypeScript: 'a.b' is not a valid path when `a` is an empty object. + // @ts-expect-error - 'a.b' does not exist on an empty object + expect(omitDeep(emptyObject, ['a.b'])).toEqual({ a: {} }); + + // Runtime: omitting a non-existent nested path is a no-op. + // TypeScript: 'a.b' is not a valid path when `a` is an empty array. + // @ts-expect-error - 'a.b' does not exist on an empty array + expect(omitDeep(emptyArray, ['a.b'])).toEqual({ a: [] }); + }); + + it('should not modify primitive values', () => { + expect(omitDeep(123, [])).toBe(123); + expect(omitDeep('string', [])).toBe('string'); + expect(omitDeep(null, [])).toBe(null); + expect(omitDeep(undefined, [])).toBe(undefined); + expect(omitDeep(true, [])).toBe(true); + }); + + it('should properly type nested objects', () => { + const object = { + user: { + id: 1, + profile: { + name: 'John', + email: 'john@example.com', + }, + }, + }; + const result = omitDeep(object, ['user.profile.email']); + + expectTypeOf(result).toExtend<{ + user: { + id: number; + profile: { + name: string; + }; + }; + }>(); + }); + + it('should properly type arrays of objects', () => { + const object = { + users: [ + { id: 1, secret: 'abc' }, + { id: 2, secret: 'def' }, + ], + }; + const result = omitDeep(object, ['users.secret']); + + expectTypeOf(result).toExtend<{ + users: Array<{ + id: number; + }>; + }>(); + }); + + it('should properly type omitted nested objects', () => { + const object = { + user: { + id: 1, + profile: { + name: 'John', + }, + }, + }; + const result = omitDeep(object, ['user.profile']); + + expectTypeOf(result).toExtend<{ + user: { + id: number; + }; + }>(); + }); +}); diff --git a/src/object/omitDeep.ts b/src/object/omitDeep.ts new file mode 100644 index 000000000..31bd756dd --- /dev/null +++ b/src/object/omitDeep.ts @@ -0,0 +1,117 @@ +type Paths = T extends object + ? T extends Array + ? Paths + : { + [K in keyof T]: K extends string + ? Prefix extends '' + ? K | Paths + : `${Prefix}.${K}` | Paths + : never; + }[keyof T] + : never; + +type Split = S extends `${infer First}.${infer Rest}` ? [First, ...Split] : [S]; + +type OmitFields = T extends object + ? T extends Array> + ? Array> + : T extends unknown[] + ? T + : { + [K in keyof T as [...CurrentPath, K] extends Split ? never : K]: OmitFields< + T[K], + Path, + [...CurrentPath, K & string] + >; + } + : T; + +export type OmitDeep = OmitFields; + +function omitDeepImpl(object: T, paths: string[], currentPath = ''): any { + if (object == null || typeof object !== 'object') { + return object; + } + + if (Array.isArray(object)) { + return object.map(item => omitDeepImpl(item, paths, currentPath)); + } + + const result: Record = {}; + + const keys = Object.keys(object); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]!; + const value = (object as Record)[key]!; + + const newPath = currentPath === '' ? key : `${currentPath}.${key}`; + + if (paths.some(path => path === newPath)) { + continue; + } + + result[key] = value !== null && typeof value === 'object' ? omitDeepImpl(value, paths, newPath) : value; + } + + return result; +} + +/** + * Creates a new object with specified nested paths omitted. + * + * This function takes an object and an array of dot-separated paths, and returns a new object that + * excludes the properties corresponding to the specified paths. Paths use dot notation to reference + * nested properties (e.g. `'user.address.city'`). + * + * @template T - The type of object. + * @template P - The type of paths to omit. + * @param object - The object to omit paths from. + * @param paths - An array of dot-separated paths to be omitted from the object. + * @returns A new object with the specified paths omitted. + * + * @example + * const obj = { a: 1, b: { x: 2, y: 3 }, c: 4 }; + * const result = omitDeep(obj, ['b.x']); + * // result will be { a: 1, b: { y: 3 }, c: 4 } + * + * @example + * // Example with nested objects + * const nested = { + * user: { + * id: 1, + * profile: { + * name: 'John', + * email: 'john@example.com' + * } + * } + * }; + * const nestedResult = omitDeep(nested, ['user.profile.email']); + * // nestedResult will be: + * // { + * // user: { + * // id: 1, + * // profile: { + * // name: 'John' + * // } + * // } + * // } + * + * @example + * // Example with arrays of objects + * const arr = { + * users: [ + * { id: 1, secret: 'abc' }, + * { id: 2, secret: 'def' } + * ] + * }; + * const arrResult = omitDeep(arr, ['users.secret']); + * // arrResult will be { + * // users: [ + * // { id: 1 }, + * // { id: 2 } + * // ] + * // } + */ +export function omitDeep>>(object: T, paths: P) { + return omitDeepImpl(object, paths as unknown as string[]) as OmitDeep; +} From 818ef61b71c81ae266ebc8be5d3b801ea0284e58 Mon Sep 17 00:00:00 2001 From: kevaldonga Date: Sun, 5 Jul 2026 15:48:12 +0530 Subject: [PATCH 2/2] fix (object/omit-deep): resolved build error - 'missing an explicit return type' --- src/object/omitDeep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/object/omitDeep.ts b/src/object/omitDeep.ts index 31bd756dd..8732789d5 100644 --- a/src/object/omitDeep.ts +++ b/src/object/omitDeep.ts @@ -112,6 +112,6 @@ function omitDeepImpl(object: T, paths: string[], currentPath = ''): any { * // ] * // } */ -export function omitDeep>>(object: T, paths: P) { +export function omitDeep>>(object: T, paths: P): OmitDeep { return omitDeepImpl(object, paths as unknown as string[]) as OmitDeep; }