import { StackAssertionError } from "./errors"; export function isNotNull(value: T): value is NonNullable { return value !== null && value !== undefined; } import.meta.vitest?.test("isNotNull", ({ expect }) => { expect(isNotNull(null)).toBe(false); expect(isNotNull(undefined)).toBe(false); expect(isNotNull(0)).toBe(true); expect(isNotNull("")).toBe(true); expect(isNotNull(false)).toBe(true); expect(isNotNull({})).toBe(true); expect(isNotNull([])).toBe(true); }); export type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T; /** * Assumes both objects are primitives, arrays, or non-function plain objects, and compares them deeply. * * Note that since they are assumed to be plain objects, this function does not compare prototypes. */ export function deepPlainEquals(obj1: T, obj2: unknown, options: { ignoreUndefinedValues?: boolean } = {}): obj2 is T { if (typeof obj1 !== typeof obj2) return false; if (obj1 === obj2) return true; switch (typeof obj1) { case 'object': { if (!obj1 || !obj2) return false; if (Array.isArray(obj1) || Array.isArray(obj2)) { if (!Array.isArray(obj1) || !Array.isArray(obj2)) return false; if (obj1.length !== obj2.length) return false; return obj1.every((v, i) => deepPlainEquals(v, obj2[i], options)); } const entries1 = Object.entries(obj1).filter(([k, v]) => !options.ignoreUndefinedValues || v !== undefined); const entries2 = Object.entries(obj2).filter(([k, v]) => !options.ignoreUndefinedValues || v !== undefined); if (entries1.length !== entries2.length) return false; return entries1.every(([k, v1]) => { const e2 = entries2.find(([k2]) => k === k2); if (!e2) return false; return deepPlainEquals(v1, e2[1], options); }); } case 'undefined': case 'string': case 'number': case 'boolean': case 'bigint': case 'symbol': case 'function':{ return false; } default: { throw new Error("Unexpected typeof " + typeof obj1); } } } import.meta.vitest?.test("deepPlainEquals", ({ expect }) => { // Simple values expect(deepPlainEquals(1, 1)).toBe(true); expect(deepPlainEquals("test", "test")).toBe(true); expect(deepPlainEquals(1, 2)).toBe(false); expect(deepPlainEquals("test", "other")).toBe(false); // Arrays expect(deepPlainEquals([1, 2, 3], [1, 2, 3])).toBe(true); expect(deepPlainEquals([1, 2, 3], [1, 2, 4])).toBe(false); expect(deepPlainEquals([1, 2, 3], [1, 2])).toBe(false); // Objects expect(deepPlainEquals({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); expect(deepPlainEquals({ a: 1, b: 2 }, { a: 1, b: 3 })).toBe(false); expect(deepPlainEquals({ a: 1, b: 2 }, { a: 1 })).toBe(false); // Nested structures expect(deepPlainEquals({ a: 1, b: [1, 2, { c: 3 }] }, { a: 1, b: [1, 2, { c: 3 }] })).toBe(true); expect(deepPlainEquals({ a: 1, b: [1, 2, { c: 3 }] }, { a: 1, b: [1, 2, { c: 4 }] })).toBe(false); // With options expect(deepPlainEquals({ a: 1, b: undefined }, { a: 1 }, { ignoreUndefinedValues: true })).toBe(true); expect(deepPlainEquals({ a: 1, b: undefined }, { a: 1 })).toBe(false); }); export function deepPlainClone(obj: T): T { if (typeof obj === 'function') throw new StackAssertionError("deepPlainClone does not support functions"); if (typeof obj === 'symbol') throw new StackAssertionError("deepPlainClone does not support symbols"); if (typeof obj !== 'object' || !obj) return obj; if (Array.isArray(obj)) return obj.map(deepPlainClone) as any; return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, deepPlainClone(v)])) as any; } import.meta.vitest?.test("deepPlainClone", ({ expect }) => { // Primitive values expect(deepPlainClone(1)).toBe(1); expect(deepPlainClone("test")).toBe("test"); expect(deepPlainClone(null)).toBe(null); expect(deepPlainClone(undefined)).toBe(undefined); // Arrays const arr = [1, 2, 3]; const clonedArr = deepPlainClone(arr); expect(clonedArr).toEqual(arr); expect(clonedArr).not.toBe(arr); // Different reference // Objects const obj = { a: 1, b: 2 }; const clonedObj = deepPlainClone(obj); expect(clonedObj).toEqual(obj); expect(clonedObj).not.toBe(obj); // Different reference // Nested structures const nested = { a: 1, b: [1, 2, { c: 3 }] }; const clonedNested = deepPlainClone(nested); expect(clonedNested).toEqual(nested); expect(clonedNested).not.toBe(nested); // Different reference expect(clonedNested.b).not.toBe(nested.b); // Different reference for nested array expect(clonedNested.b[2]).not.toBe(nested.b[2]); // Different reference for nested object // Error cases expect(() => deepPlainClone(() => {})).toThrow(); expect(() => deepPlainClone(Symbol())).toThrow(); }); export function typedEntries(obj: T): [keyof T, T[keyof T]][] { return Object.entries(obj) as any; } import.meta.vitest?.test("typedEntries", ({ expect }) => { expect(typedEntries({})).toEqual([]); expect(typedEntries({ a: 1, b: 2 })).toEqual([["a", 1], ["b", 2]]); expect(typedEntries({ a: "hello", b: true, c: null })).toEqual([["a", "hello"], ["b", true], ["c", null]]); // Test with object containing methods const objWithMethod = { a: 1, b: () => "test" }; const entries = typedEntries(objWithMethod); expect(entries.length).toBe(2); expect(entries[0][0]).toBe("a"); expect(entries[0][1]).toBe(1); expect(entries[1][0]).toBe("b"); expect(typeof entries[1][1]).toBe("function"); }); export function typedFromEntries(entries: [K, V][]): Record { return Object.fromEntries(entries) as any; } import.meta.vitest?.test("typedFromEntries", ({ expect }) => { expect(typedFromEntries([])).toEqual({}); expect(typedFromEntries([["a", 1], ["b", 2]])).toEqual({ a: 1, b: 2 }); // Test with mixed types (using type assertion) const mixedEntries = [["a", "hello"], ["b", true], ["c", null]] as [string, string | boolean | null][]; const mixedObj = typedFromEntries(mixedEntries); expect(mixedObj).toEqual({ a: "hello", b: true, c: null }); // Test with function values const fn = () => "test"; type MixedValue = number | (() => string); const fnEntries: [string, MixedValue][] = [["a", 1], ["b", fn]]; const obj = typedFromEntries(fnEntries); expect(obj.a).toBe(1); expect(typeof obj.b).toBe("function"); // Type assertion needed for the function call expect((obj.b as () => string)()).toBe("test"); }); export function typedKeys(obj: T): (keyof T)[] { return Object.keys(obj) as any; } import.meta.vitest?.test("typedKeys", ({ expect }) => { expect(typedKeys({})).toEqual([]); expect(typedKeys({ a: 1, b: 2 })).toEqual(["a", "b"]); expect(typedKeys({ a: "hello", b: true, c: null })).toEqual(["a", "b", "c"]); // Test with object containing methods const objWithMethod = { a: 1, b: () => "test" }; expect(typedKeys(objWithMethod)).toEqual(["a", "b"]); }); export function typedValues(obj: T): T[keyof T][] { return Object.values(obj) as any; } import.meta.vitest?.test("typedValues", ({ expect }) => { expect(typedValues({})).toEqual([]); expect(typedValues({ a: 1, b: 2 })).toEqual([1, 2]); // Test with mixed types type MixedObj = { a: string, b: boolean, c: null }; const mixedObj: MixedObj = { a: "hello", b: true, c: null }; expect(typedValues(mixedObj)).toEqual(["hello", true, null]); // Test with object containing methods type ObjWithFn = { a: number, b: () => string }; const fn = () => "test"; const objWithMethod: ObjWithFn = { a: 1, b: fn }; const values = typedValues(objWithMethod); expect(values.length).toBe(2); expect(values[0]).toBe(1); expect(typeof values[1]).toBe("function"); // Need to cast to the correct type const fnValue = values[1] as () => string; expect(fnValue()).toBe("test"); }); export function typedAssign(target: T, source: U): T & U { return Object.assign(target, source); } import.meta.vitest?.test("typedAssign", ({ expect }) => { // Test with empty objects const emptyTarget = {}; const emptyResult = typedAssign(emptyTarget, { a: 1 }); expect(emptyResult).toEqual({ a: 1 }); expect(emptyResult).toBe(emptyTarget); // Same reference // Test with non-empty target const target = { a: 1, b: 2 }; const result = typedAssign(target, { c: 3, d: 4 }); expect(result).toEqual({ a: 1, b: 2, c: 3, d: 4 }); expect(result).toBe(target); // Same reference // Test with overlapping properties const targetWithOverlap = { a: 1, b: 2 }; const resultWithOverlap = typedAssign(targetWithOverlap, { b: 3, c: 4 }); expect(resultWithOverlap).toEqual({ a: 1, b: 3, c: 4 }); expect(resultWithOverlap).toBe(targetWithOverlap); // Same reference }); export type FilterUndefined = & { [k in keyof T as (undefined extends T[k] ? (T[k] extends undefined | void ? never : k) : never)]+?: T[k] & ({} | null) } & { [k in keyof T as (undefined extends T[k] ? never : k)]: T[k] & ({} | null) } /** * Returns a new object with all undefined values removed. Useful when spreading optional parameters on an object, as * TypeScript's `Partial` type allows `undefined` values. */ export function filterUndefined(obj: T): FilterUndefined { return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as any; } import.meta.vitest?.test("filterUndefined", ({ expect }) => { expect(filterUndefined({})).toEqual({}); expect(filterUndefined({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }); expect(filterUndefined({ a: 1, b: undefined })).toEqual({ a: 1 }); expect(filterUndefined({ a: undefined, b: undefined })).toEqual({}); expect(filterUndefined({ a: null, b: undefined })).toEqual({ a: null }); expect(filterUndefined({ a: 0, b: "", c: false, d: undefined })).toEqual({ a: 0, b: "", c: false }); }); export type FilterUndefinedOrNull = FilterUndefined<{ [k in keyof T]: null extends T[k] ? NonNullable | undefined : T[k] }>; /** * Returns a new object with all undefined and null values removed. Useful when spreading optional parameters on an object, as * TypeScript's `Partial` type allows `undefined` values. */ export function filterUndefinedOrNull(obj: T): FilterUndefinedOrNull { return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null)) as any; } import.meta.vitest?.test("filterUndefinedOrNull", ({ expect }) => { expect(filterUndefinedOrNull({})).toEqual({}); expect(filterUndefinedOrNull({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }); }); export function pick(obj: T, keys: K[]): Pick { return Object.fromEntries(Object.entries(obj).filter(([k]) => keys.includes(k as K))) as any; } import.meta.vitest?.test("pick", ({ expect }) => { const obj = { a: 1, b: 2, c: 3, d: 4 }; expect(pick(obj, ["a", "c"])).toEqual({ a: 1, c: 3 }); expect(pick(obj, [])).toEqual({}); expect(pick(obj, ["a", "e" as keyof typeof obj])).toEqual({ a: 1 }); // Use type assertion for empty object to avoid TypeScript error expect(pick({} as Record, ["a"])).toEqual({}); }); export function omit(obj: T, keys: K[]): Omit { return Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k as K))) as any; } import.meta.vitest?.test("omit", ({ expect }) => { const obj = { a: 1, b: 2, c: 3, d: 4 }; expect(omit(obj, ["a", "c"])).toEqual({ b: 2, d: 4 }); expect(omit(obj, [])).toEqual(obj); expect(omit(obj, ["a", "e" as keyof typeof obj])).toEqual({ b: 2, c: 3, d: 4 }); // Use type assertion for empty object to avoid TypeScript error expect(omit({} as Record, ["a"])).toEqual({}); }); export function split(obj: T, keys: K[]): [Pick, Omit] { return [pick(obj, keys), omit(obj, keys)]; } import.meta.vitest?.test("split", ({ expect }) => { const obj = { a: 1, b: 2, c: 3, d: 4 }; expect(split(obj, ["a", "c"])).toEqual([{ a: 1, c: 3 }, { b: 2, d: 4 }]); expect(split(obj, [])).toEqual([{}, obj]); expect(split(obj, ["a", "e" as keyof typeof obj])).toEqual([{ a: 1 }, { b: 2, c: 3, d: 4 }]); // Use type assertion for empty object to avoid TypeScript error expect(split({} as Record, ["a"])).toEqual([{}, {}]); }); export function set(obj: T, key: K, value: T[K]) { Object.defineProperty(obj, key, { value, writable: true, configurable: true, enumerable: true }); } export function get(obj: T, key: K): T[K] { const descriptor = Object.getOwnPropertyDescriptor(obj, key); if (!descriptor) throw new StackAssertionError(`get: key ${String(key)} does not exist`, { obj, key }); return descriptor.value; } export function has(obj: T, key: K): obj is T { return Object.prototype.hasOwnProperty.call(obj, key); } export function deleteKey(obj: T, key: K) { if (has(obj, key)) { Reflect.deleteProperty(obj, key); } else { throw new StackAssertionError(`deleteKey: key ${String(key)} does not exist`, { obj, key }); } }