From ac36679c2baa56c344a32b571e7156934dc462cb Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:43:21 -0400 Subject: [PATCH] stop using objectify and improve the return type --- src/object/crush.ts | 79 +++++++++++++++++++++++++++------------------ src/types.ts | 16 +++++++++ 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/object/crush.ts b/src/object/crush.ts index d2835826..36298a5b 100644 --- a/src/object/crush.ts +++ b/src/object/crush.ts @@ -1,4 +1,4 @@ -import { isDate, isPrimitive, objectify } from 'radashi' +import { type Intersect, isArray, isObject, type Simplify } from 'radashi' /** * Flattens a deep object to a single dimension, converting the keys @@ -11,37 +11,54 @@ import { isDate, isPrimitive, objectify } from 'radashi' * // { name: 'ra', 'children.0.name': 'hathor' } * ``` */ -type Primitive = - | number - | string - | boolean - | Date - | symbol - | bigint - | undefined - | null - -const crushToPvArray: ( - obj: object, - path: string, -) => Array<{ p: string; v: Primitive }> = (obj: object, path: string) => - Object.entries(obj).flatMap(([key, value]) => - isPrimitive(value) || isDate(value) - ? { p: path === '' ? key : `${path}.${key}`, v: value } - : crushToPvArray(value, path === '' ? key : `${path}.${key}`), - ) - -export function crush( - value: TValue, -): Record | Record { +export function crush(value: T): Crush { if (!value) { return {} } - - const result = objectify( - crushToPvArray(value, ''), - o => o.p, - o => o.v, - ) - return result + return (function crushReducer( + crushed: Crush, + value: unknown, + path: string, + ) { + if (isObject(value) || isArray(value)) { + for (const [prop, propValue] of Object.entries(value)) { + crushReducer(crushed, propValue, path ? `${path}.${prop}` : prop) + } + } else { + crushed[path as keyof Crush] = value as Crush[keyof Crush] + } + return crushed + })({} as Crush, value, '') } + +/** + * The return type of the `crush` function. + * + * This type is limited by TypeScript's development. There's no + * reliable way to detect if an object will pass `isObject` or not, so + * we cannot infer the property types of nested objects that have been + * crushed. + * + * @see https://radashi-org.github.io/reference/object/crush + */ +export type Crush = T extends readonly (infer U)[] + ? Record + : Simplify< + Intersect< + keyof T extends infer Prop + ? Prop extends keyof T + ? T[Prop] extends infer Value + ? + | ([Extract] extends [never] + ? never + : Record) + | ([Exclude] extends [never] + ? never + : [Extract] extends [never] + ? { [P in Prop]: Value } + : Record) + : never + : never + : never + > + > diff --git a/src/types.ts b/src/types.ts index 5234cb95..20baf2c4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -108,3 +108,19 @@ export type ComparableProperty = CompatibleProperty * value, and 0 to keep the order of the values. */ export type Comparator = (left: T, right: T) => number + +/** Convert a union to an intersection */ +export type Intersect = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never + +/** + * Useful to flatten the type output to improve type hints shown in + * editors. And also to transform an interface into a type to aide + * with assignability. + * + * @see https://github.com/microsoft/TypeScript/issues/15300 + */ +export type Simplify = {} & { [P in keyof T]: T[P] }