Deep Omit Objects by Dot Key Paths
Clone nested plain data while removing fields at dot-separated paths, with forbidden-key guards against prototype pollution.
Code
type PathTree = Map<string, PathTree>;
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
export function omitDeep<Value>(value: Value, paths: readonly string[]): Value {
if (!Array.isArray(paths)) throw new TypeError("paths must be an array");
if (paths.length === 0) return value;
const tree: PathTree = new Map();
for (const path of paths) {
if (typeof path !== "string" || path.trim() === "") {
throw new TypeError("paths must be non-empty strings");
}
const segments = path.split(".").map((segment) => segment.trim());
if (segments.some((segment) => segment === "")) throw new Error(`invalid path "${path}"`);
for (const segment of segments) {
if (FORBIDDEN_KEYS.has(segment)) throw new Error(`path "${path}" contains a forbidden key`);
}
insert(tree, segments);
}
return clone(value, tree) as Value;
}
function insert(tree: PathTree, segments: readonly string[]): void {
let node = tree;
for (const segment of segments) {
let child = node.get(segment);
if (child === undefined) {
child = new Map();
node.set(segment, child);
}
node = child;
}
}
function clone(value: unknown, tree: PathTree): unknown {
if (Array.isArray(value)) {
return value.map((item, index) => {
const child = tree.get(String(index));
if (child !== undefined && child.size === 0) return undefined;
return clone(item, child ?? tree);
});
}
if (value !== null && typeof value === "object") {
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("omitDeep only supports plain objects and arrays");
}
const output: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
const child = tree.get(key);
if (child !== undefined && child.size === 0) continue;
output[key] = clone(item, child ?? new Map());
}
return output;
}
return value;
}
const source = {
id: 1,
password: "secret",
profile: { email: "[email protected]", token: "abc", nested: { keep: true } },
items: [{ id: 1, token: "a" }, { id: 2, token: "b" }],
};
const sanitized = omitDeep(source, ["password", "profile.token", "items.token"]);
console.log(JSON.stringify(sanitized));
console.log(JSON.stringify(source));Report a problem with this exampleOpens a pre-filled issue on GitHub.