export type StripCommand = string | RegExp; /** * Recursively removes properties from objects based on key patterns. * * Strips properties whose keys or paths match the provided string/regex patterns. * Supports both shallow key matching and deep path matching (e.g., "user.password"). * Handles arrays and nested objects, preventing circular reference issues. * * @template T - Input type (object or array) * @template S - Element type for arrays * @param strip - String, RegExp, or array thereof specifying keys/paths to remove * @param obj - Object or array to strip properties from * @returns New object/array with matching properties removed * * @example * ```typescript * const data = { * name: 'Alice', * password: 'secret', * nested: { apiKey: '123', value: 42 } * }; * * // Remove by key name * const safe1 = stripper('password', data); * // { name: 'Alice', nested: { apiKey: '123', value: 42 } } * * // Remove by path * const safe2 = stripper('nested.apiKey', data); * // { name: 'Alice', password: 'secret', nested: { value: 42 } } * * // Remove multiple with regex * const safe3 = stripper([/password/, /apiKey/], data); * // { name: 'Alice', nested: { value: 42 } } * ``` */ export function stripper | S, S>( strip: StripCommand | StripCommand[], obj: T, ): T extends ArrayLike ? Record[] : Record { const strips = Array.isArray(strip) ? strip : [strip]; const restrips = strips.map((s) => { if (typeof s === "string") { const escaped = s.replace(/[-\\[\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\^\\$\\|]/g, "\\$&"); return new RegExp(`^${escaped}$`); } return s; }); const selfRef = new WeakSet(); return localStripper(undefined, restrips, obj, selfRef) as T extends ArrayLike ? Record[] : Record; } function localStripper(path: string | undefined, restrips: RegExp[], obj: T, selfRef: WeakSet): unknown { if (typeof obj !== "object" || obj === null) { return obj; } if (selfRef.has(obj)) { return obj; } selfRef.add(obj); if (Array.isArray(obj)) { return obj.map((i) => localStripper(path, restrips, i, selfRef)); } const ret = { ...obj } as Record; const matcher = (key: string, nextPath: string): boolean => { for (const re of restrips) { if (re.test(key) || re.test(nextPath)) { return true; } } return false; }; for (const key in ret) { if (Object.prototype.hasOwnProperty.call(ret, key)) { let nextPath: string; if (path) { nextPath = [path, key].join("."); } else { nextPath = key; } if (matcher(key, nextPath)) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete ret[key]; continue; } if (typeof ret[key] === "object") { if (Array.isArray(ret[key])) { ret[key] = ret[key].reduce((acc: unknown[], v, i) => { const toDelete = matcher(key, `${nextPath}[${i}]`); if (!toDelete) { acc.push(localStripper(`${nextPath}[${i}]`, restrips, v, selfRef)); } return acc; }, []); } else { ret[key] = localStripper(nextPath, restrips, ret[key], selfRef); } } } } return ret; }