/* eslint-disable @typescript-eslint/no-explicit-any */ import { isEqualWith } from 'lodash-es' import { areEqualArraysWithoutOrder } from './arrays' import type { Prettify } from 'ts-essentials' import type TB from 'ts-toolbelt' export function removeUndefined(obj: Record) { return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) } type RemoveUndefinedProps> = { [key in keyof T]-?: Exclude } /** * Assigns properties from `obj2` to `obj1`, but only if they are defined in `obj2`. * @example * assignDefinedOnly({ a: 1, b: 2}, { a: undefined, b: 3, c: 4 }) // result = { a: 1, b: 3, c: 4 } */ export const assignDefinedOnly = , T2 extends Record>( obj1: T1, obj2: T2 ) => { return { ...obj1, ...removeUndefined(obj2) } as Omit, keyof T1> & Omit & { [key in keyof T1 & keyof T2]-?: undefined extends T2[key] ? Exclude | T1[key] : T2[key] } } export type Paths = T extends object ? { [K in keyof T]: `${Exclude}${'' | `.${Paths}`}` }[keyof T] : never export type PathValue = TB.Object.Path> export type Leaves = T extends object ? { [K in keyof T]: `${Exclude}${Leaves extends never ? '' : `.${Leaves}`}` }[keyof T] : never // Start of paths with nested type Digit = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 type NextDigit = [1, 2, 3, 4, 5, 6, 7, 'STOP'] type Inc = T extends Digit ? NextDigit[T] : 'STOP' type StringOrNumKeys = TObj extends unknown[] ? 0 : string & keyof TObj type NestedPath = TValue extends object ? `${Prefix}.${TDepth extends 'STOP' ? string : NestedFieldPaths}` : never type GetValue = T extends unknown[] ? K extends number ? T[K] : never : K extends keyof T ? T[K] : never type NestedFieldPaths = { [TKey in StringOrNumKeys]: | NestedPath, `${TKey}`, TValue, Inc> | (GetValue extends TValue ? `${TKey}` : never) }[StringOrNumKeys] export type PathsWithNested = TData extends any ? NestedFieldPaths : never // End of paths with nested export type TypedGroupBy & Record> = { [Id in T[K]]: Extract> } /** * Converts an array of objects to an object with the key as the id and the value as the object. * @example * typedGroupBy([ * { id: 'a', name: 'Letter A' }, * { id: 'b', name: 'Letter B' } * ] as const, 'id') * // result = { * // a: { id: 'a', name: 'Letter A' }, * // b: { id: 'b', name: 'Letter B' } * // } */ export const typedGroupBy = & Record>( array: T[] | readonly T[], key: K ) => { return Object.fromEntries(array.map((option) => [option[key], option])) as TypedGroupBy } /** * Merges two objects, so that each property is the union of that property from each object. * - If a key is present in only one of the objects, it becomes optional. * - If an object is undefined, the other object is returned. * * To {@link UnionizeTwo} more than two objects, use {@link Unionize}. * * @example * UnionizeTwo<{ a: 1, shared: 1 }, { b: 2, shared: 2 }> // { a?: 1, b?: 2, shared: 1 | 2 } */ export type UnionizeTwo< T1 extends Record | undefined, T2 extends Record | undefined, > = keyof T1 extends undefined ? T2 : keyof T2 extends undefined ? T1 : { [K in Exclude]+?: | (K extends keyof T1 ? T1[K] : never) | (K extends keyof T2 ? T2[K] : never) } & { [K in keyof T1 & keyof T2]-?: T1[K] | T2[K] } /** * Merges multiple objects, so that each property is the union of that property from each object. * - If a key is present in only one of the objects, it becomes optional. * - If an object is undefined, it is ignored. * - If no objects are provided, `undefined` is returned. * * Internally, it uses {@link UnionizeTwo} recursively. * * @example * Unionize<[ * { a: 1, shared: 1 }, * { b: 2, shared: 2 }, * { a: 3, shared: 3 } * ]> * // result = { * // a?: 1 | 3, * // b?: 2, * // shared: 1 | 2 | 3 * // } */ export type Unionize[]> = Prettify< T extends [] ? undefined : T extends [infer First, ...infer Rest] ? Rest extends Record[] ? First extends Record ? UnionizeTwo> : undefined : First : undefined > /** * Checks if two objects are equal without considering order in arrays. * @example * areEqualObjectsWithoutOrder({ a: [1, 2], b: 3 }, { b: 3, a: [2, 1] }) // true */ export function areEqualObjectsWithoutOrder>( a: T, b: Record ): b is T { return isEqualWith(a, b, (a, b) => { if (Array.isArray(a) && Array.isArray(b)) return areEqualArraysWithoutOrder(a, b) return undefined }) } /** * Same as {@link Object.entries}, but with proper typing. * @example * typedObjectEntries({ a: 1, b: 2 }) // [['a', 1], ['b', 2]] */ export function typedObjectEntries>(obj: T) { return Object.entries(obj) as Prettify< { [K in Extract]: [K, T[K]] }[Extract] >[] }