compactObject
EditRecursively removes null and undefined properties and array entries.
type Nullish = null | undefined;type AtomicObject = | Date | RegExp | Error | Promise<unknown> | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | WeakMap<object, unknown> | WeakSet<object> | ArrayBuffer | ArrayBufferView;
type IsAny<T> = 0 extends 1 & T ? true : false;type IsAlwaysNullish<T> = [Exclude<T, Nullish>] extends [never] ? true : false;type MayBeNullish<T> = IsAny<T> extends true ? true : [null] extends [T] ? true : [undefined] extends [T] ? true : false;type Simplify<T> = { [Key in keyof T]: T[Key] };type ArrayExtraKeys<T extends readonly unknown[]> = Exclude< keyof T, keyof readonly unknown[] | number | `${number}`>;
type CompactProperties<T extends object> = Simplify< { [Key in keyof T as IsAlwaysNullish<T[Key]> extends true ? never : MayBeNullish<T[Key]> extends true ? never : Key]: CompactResult<Exclude<T[Key], Nullish>>; } & { [Key in keyof T as IsAlwaysNullish<T[Key]> extends true ? never : MayBeNullish<T[Key]> extends true ? Key : never]?: CompactResult<Exclude<T[Key], Nullish>>; }>;type CompactArray<T extends readonly unknown[]> = Array< CompactResult<Exclude<T[number], Nullish>>> & CompactProperties<Pick<T, ArrayExtraKeys<T>>>;
/** * The recursively compacted form of a value. * * Nullish-only properties disappear, possibly nullish properties become * optional, and nullish array members are removed. */export type CompactResult<T> = T extends Nullish ? never : T extends (...arguments_: never[]) => unknown ? T : T extends readonly unknown[] ? CompactArray<T> : T extends AtomicObject ? T : T extends object ? T extends Record<PropertyKey, unknown> ? CompactProperties<T> : T | CompactProperties<T> : T;
const isArrayIndex = (key: PropertyKey): boolean => { if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key)) return false; const index = Number(key); return Number.isSafeInteger(index) && index < 4_294_967_295;};
const readDescriptorValue = ( descriptor: PropertyDescriptor, receiver: object): unknown => { if (Object.hasOwn(descriptor, "value")) return descriptor.value; return descriptor.get ? Reflect.apply(descriptor.get, receiver, []) : undefined;};
const snapshotOwnValues = (object: object) => { const properties = Reflect.ownKeys(object).flatMap((key) => { const descriptor = Reflect.getOwnPropertyDescriptor(object, key); return descriptor ? [{ key, descriptor }] : []; }); return properties.map(({ key, descriptor }) => ({ key, value: readDescriptorValue(descriptor, object), enumerable: descriptor.enumerable === true, }));};
const compactValue = (value: unknown, seen: WeakMap<object, unknown>): unknown => { if (value == null || typeof value !== "object") return value; const prototype = Object.getPrototypeOf(value); if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return value; const cached = seen.get(value); if (cached !== undefined) return cached;
if (Array.isArray(value)) { const result: unknown[] = []; seen.set(value, result); const entries = snapshotOwnValues(value); const indexedEntries = entries .filter(({ key }) => isArrayIndex(key)) .sort((left, right) => Number(left.key) - Number(right.key)); for (const { value: item } of indexedEntries) { if (item != null) result.push(compactValue(item, seen)); } for (const { key, value: item, enumerable } of entries) { if ( key === "length" || isArrayIndex(key) ) { continue; } if (item != null) { Object.defineProperty(result, key, { value: compactValue(item, seen), enumerable, configurable: true, writable: true, }); } } return result; }
const result: Record<PropertyKey, unknown> = Object.create(prototype); seen.set(value, result); const entries = snapshotOwnValues(value); for (const { key, value: item, enumerable } of entries) { if (item != null) { Reflect.defineProperty(result, key, { value: compactValue(item, seen), enumerable, configurable: true, writable: true, }); } } return result;};
/** Recursively removes null and undefined own properties and array entries. */export const compactObject = <T extends object>(object: T): CompactResult<T> => { return compactValue(object, new WeakMap()) as CompactResult<T>;};Download
Section titled “Download”wget -O src/lib/compactObject.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/functions/compactObject.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/functions "$temp_dir" && mv "$temp_dir/compactObject.ts" src/lib/compactObject.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { compactObject } from "./compactObject";
const payload = compactObject({ name: "Ada", nickname: null, middleName: undefined as string | undefined, flags: [true, undefined, false],});
// `nickname` is removed, `middleName` is optional, and flags is boolean[].console.log(payload);object(T) — Plain object or array to copy recursively.
Returns
Section titled “Returns”CompactResult<T> — A recursive copy where nullish-only properties are
removed, possibly nullish properties are optional, and array item types exclude
null and undefined. 0, false, and empty strings remain.
Arrays and plain objects are compacted. Dates, regular expressions, promises,
collection objects, typed arrays, and custom class instances remain unchanged
as leaf values. Circular references, symbol keys, null prototypes, and own
special keys such as __proto__ are preserved safely. Non-enumerable
properties remain non-enumerable when their values survive compaction.