From 7f8185e1e2387c904a27336c2e4aa145b9b85c26 Mon Sep 17 00:00:00 2001 From: queicherius Date: Thu, 31 Oct 2024 23:11:00 +0000 Subject: [PATCH 1/3] Add unflatten function --- README.md | 11 +++++++++ src/index.ts | 1 + src/typeHelpers.ts | 14 +++++++++++ src/unflatten/unflatten.spec.ts | 25 +++++++++++++++++++ src/unflatten/unflatten.ts | 44 +++++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 src/unflatten/unflatten.spec.ts create mode 100644 src/unflatten/unflatten.ts diff --git a/README.md b/README.md index a650562f..dced74f8 100644 --- a/README.md +++ b/README.md @@ -518,6 +518,17 @@ flocky.toMap( [Source](./src/toMap/toMap.ts) • Minify: 95 B • Minify & GZIP: 95 B +### unflatten(object) + +Unflattens an object with dot notation keys into a nested object. + +```js +flocky.unflatten({ 'a.b': 1, 'a.c': 2, 'd.e.f': 3 }) +// -> { a: { b: 1, c: 2 }, d: { e: { f: 3 } } } +``` + +[Source](./src/unflatten/unflatten.ts) • Minify: 199 B • Minify & GZIP: 145 B + ### unique(array, identity?) Create a duplicate-free version of an array, in which only the first occurrence of each element is kept. diff --git a/src/index.ts b/src/index.ts index 396b7634..fcc38a11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,4 +28,5 @@ export { slugify } from './slugify/slugify' export { sum } from './sum/sum' export { throttle } from './throttle/throttle' export { toMap } from './toMap/toMap' +export { unflatten } from './unflatten/unflatten' export { unique } from './unique/unique' diff --git a/src/typeHelpers.ts b/src/typeHelpers.ts index 4bc0a1ec..33d1b257 100644 --- a/src/typeHelpers.ts +++ b/src/typeHelpers.ts @@ -4,3 +4,17 @@ type JSONArray = Array // eslint-disable-next-line @typescript-eslint/no-explicit-any export type TAnyFunction = (...args: any[]) => TReturn + +export type Simplify = T extends unknown ? { [K in keyof T]: Simplify } : never + +export type RecursiveUnionToIntersection = { + [TKey in keyof TObjectWithUnions]: TObjectWithUnions[TKey] extends object + ? RecursiveUnionToIntersection> + : TObjectWithUnions[TKey] +} + +export type UnionToIntersection = ( + TUnion extends any ? (k: TUnion) => void : never +) extends (k: infer TIntersection) => void + ? TIntersection + : never diff --git a/src/unflatten/unflatten.spec.ts b/src/unflatten/unflatten.spec.ts new file mode 100644 index 00000000..acf3dd43 --- /dev/null +++ b/src/unflatten/unflatten.spec.ts @@ -0,0 +1,25 @@ +import { unflatten } from './unflatten' + +describe('unflatten', () => { + test('unflattens the object', () => { + expect(unflatten({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }) + + expect(unflatten({ 'a.b': 1 })).toEqual({ a: { b: 1 } }) + expect(unflatten({ 'a.b': 1, 'a.c': 2 })).toEqual({ a: { b: 1, c: 2 } }) + expect(unflatten({ 'a.b': 1, 'a.c': 2, 'd.e': 3 })).toEqual({ a: { b: 1, c: 2 }, d: { e: 3 } }) + + expect(unflatten({ 'a.b.c': 1 })).toEqual({ a: { b: { c: 1 } } }) + expect(unflatten({ 'a.b.c': 1, 'a.b.d': 2 })).toEqual({ a: { b: { c: 1, d: 2 } } }) + expect(unflatten({ 'a.b.c': 1, 'a.b.d': 2, 'e.f.g': 3 })).toEqual({ + a: { b: { c: 1, d: 2 } }, + e: { f: { g: 3 } }, + }) + + // Check that the types are correct + const result = unflatten({ 'a.a.a': 1, 'a.b.c': 1, 'a.b.d': 2, 'e.f.g': 3 }) + expect(result.a.a.a + 1).toEqual(2) + expect(result.a.b.c + 1).toEqual(2) + expect(result.a.b.d + 1).toEqual(3) + expect(result.e.f.g + 1).toEqual(4) + }) +}) diff --git a/src/unflatten/unflatten.ts b/src/unflatten/unflatten.ts new file mode 100644 index 00000000..9273e291 --- /dev/null +++ b/src/unflatten/unflatten.ts @@ -0,0 +1,44 @@ +import { RecursiveUnionToIntersection, Simplify } from '../typeHelpers' + +/** + * ### unflatten(object) + * + * Unflattens an object with dot notation keys into a nested object. + * + * ```js + * flocky.unflatten({ 'a.b': 1, 'a.c': 2, 'd.e.f': 3 }) + * // -> { a: { b: 1, c: 2 }, d: { e: { f: 3 } } } + * ``` + */ + +type UnflattedObject> = { + [TKey in keyof TObject as TKey extends `${infer TKeyPrefix}.${string}` + ? TKeyPrefix + : TKey]: TKey extends `${string}.${infer TKeySuffix}` + ? UnflattedObject<{ [key in TKeySuffix]: TObject[TKey] }> + : TObject[TKey] +} extends infer TResult + ? { [TResultKey in keyof TResult]: TResult[TResultKey] } + : never + +export function unflatten>(object: TObject) { + const result: any = {} + + for (const key in object) { + const keys = key.split('.') + let current = result + + for (let i = 0; i < keys.length; i++) { + const part = keys[i] + + if (i === keys.length - 1) { + current[part] = object[key] + } else { + if (!current[part]) current[part] = {} + current = current[part] + } + } + } + + return result as Simplify>> +} From 80fd1200065111173818b855d2ed1361d8133334 Mon Sep 17 00:00:00 2001 From: queicherius Date: Thu, 31 Oct 2024 23:15:51 +0000 Subject: [PATCH 2/3] Add flatten function --- README.md | 11 +++++++ src/flatten/flatten.spec.ts | 25 +++++++++++++++ src/flatten/flatten.ts | 61 +++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/unflatten/unflatten.ts | 6 ++-- 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/flatten/flatten.spec.ts create mode 100644 src/flatten/flatten.ts diff --git a/README.md b/README.md index dced74f8..657242d2 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,17 @@ flocky.escapeRegExp('Hey. (1 + 1 = 2)') [Source](./src/escapeRegExp/escapeRegExp.ts) • Minify: 93 B • Minify & GZIP: 90 B +### flatten(object) + +Flattens a nested object into an object with dot notation keys. + +```js +flocky.flatten({ a: { b: 1, c: 2 }, d: { e: { f: 3 } } }) +// -> { 'a.b': 1, 'a.c': 2, 'd.e.f': 3 } +``` + +[Source](./src/flatten/flatten.ts) • Minify: 172 B • Minify & GZIP: 136 B + ### get(object, path, defaultValue?) Get the value at a `path` of an `object` (with an optional `defaultValue`) diff --git a/src/flatten/flatten.spec.ts b/src/flatten/flatten.spec.ts new file mode 100644 index 00000000..2cc1981d --- /dev/null +++ b/src/flatten/flatten.spec.ts @@ -0,0 +1,25 @@ +import { flatten } from './flatten' + +describe('flatten', () => { + test('flattens the object', () => { + expect(flatten({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }) + + expect(flatten({ a: { b: 1 } })).toEqual({ 'a.b': 1 }) + expect(flatten({ a: { b: 1, c: 2 } })).toEqual({ 'a.b': 1, 'a.c': 2 }) + expect(flatten({ a: { b: 1, c: 2 }, d: { e: 3 } })).toEqual({ 'a.b': 1, 'a.c': 2, 'd.e': 3 }) + + expect(flatten({ a: { b: { c: 1 } } })).toEqual({ 'a.b.c': 1 }) + expect(flatten({ a: { b: { c: 1, d: 2 } } })).toEqual({ 'a.b.c': 1, 'a.b.d': 2 }) + expect(flatten({ a: { b: { c: 1, d: 2 } }, e: { f: { g: 3 } } })).toEqual({ + 'a.b.c': 1, + 'a.b.d': 2, + 'e.f.g': 3, + }) + + // Check that the types are correct + const result = flatten({ a: { b: { c: 1, d: 2 } }, e: { f: { g: 3 } } }) + expect(result['a.b.c'] + 1).toEqual(2) + expect(result['a.b.d'] + 1).toEqual(3) + expect(result['e.f.g'] + 1).toEqual(4) + }) +}) diff --git a/src/flatten/flatten.ts b/src/flatten/flatten.ts new file mode 100644 index 00000000..938953b1 --- /dev/null +++ b/src/flatten/flatten.ts @@ -0,0 +1,61 @@ +/** + * ### flatten(object) + * + * Flattens a nested object into an object with dot notation keys. + * + * ```js + * flocky.flatten({ a: { b: 1, c: 2 }, d: { e: { f: 3 } } }) + * // -> { 'a.b': 1, 'a.c': 2, 'd.e.f': 3 } + * ``` + */ + +type Entry = { key: string; value: any; optional: boolean } + +type Explode = _Explode + +type _Explode = T extends object + ? { + [K in keyof T]-?: K extends string + ? Explode extends infer E + ? E extends Entry + ? { + key: `${K}${E['key'] extends '' ? '' : '.'}${E['key']}` + value: E['value'] + optional: E['key'] extends '' + ? {} extends Pick + ? true + : false + : E['optional'] + } + : never + : never + : never + }[keyof T] + : { key: ''; value: T; optional: false } + +type Collapse = { + [E in Extract as E['key']]: E['value'] +} & Partial<{ [E in Extract as E['key']]: E['value'] }> extends infer O + ? { [K in keyof O]: O[K] } + : never + +type FlattenObject = Collapse> + +export function flatten>(object: TObject) { + function recurse(current: Record, prefix = '') { + for (const key in current) { + const value = current[key] + const nextKey = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'object' && value !== null) { + recurse(value as Record, nextKey) + } else { + result[nextKey] = value + } + } + } + + const result: Record = {} + recurse(object) + return result as FlattenObject +} diff --git a/src/index.ts b/src/index.ts index fcc38a11..471638d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export { compact } from './compact/compact' export { debounce } from './debounce/debounce' export { duplicates } from './duplicates/duplicates' export { escapeRegExp } from './escapeRegExp/escapeRegExp' +export { flatten } from './flatten/flatten' export { get } from './get/get' export { hash } from './hash/hash' export { identifier } from './identifier/identifier' diff --git a/src/unflatten/unflatten.ts b/src/unflatten/unflatten.ts index 9273e291..fc098b8a 100644 --- a/src/unflatten/unflatten.ts +++ b/src/unflatten/unflatten.ts @@ -11,11 +11,11 @@ import { RecursiveUnionToIntersection, Simplify } from '../typeHelpers' * ``` */ -type UnflattedObject> = { +type UnflattenObject> = { [TKey in keyof TObject as TKey extends `${infer TKeyPrefix}.${string}` ? TKeyPrefix : TKey]: TKey extends `${string}.${infer TKeySuffix}` - ? UnflattedObject<{ [key in TKeySuffix]: TObject[TKey] }> + ? UnflattenObject<{ [key in TKeySuffix]: TObject[TKey] }> : TObject[TKey] } extends infer TResult ? { [TResultKey in keyof TResult]: TResult[TResultKey] } @@ -40,5 +40,5 @@ export function unflatten>(object: TObje } } - return result as Simplify>> + return result as Simplify>> } From 4279dc69537119e5bc580e01cce05ffa58f6d1d1 Mon Sep 17 00:00:00 2001 From: queicherius Date: Thu, 31 Oct 2024 23:21:11 +0000 Subject: [PATCH 3/3] Fix lint errors --- src/flatten/flatten.ts | 9 +++++---- src/typeHelpers.ts | 3 ++- src/unflatten/unflatten.ts | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/flatten/flatten.ts b/src/flatten/flatten.ts index 938953b1..d718476c 100644 --- a/src/flatten/flatten.ts +++ b/src/flatten/flatten.ts @@ -9,9 +9,9 @@ * ``` */ -type Entry = { key: string; value: any; optional: boolean } +type Entry = { key: string; value: unknown; optional: boolean } -type Explode = _Explode +type Explode = _Explode type _Explode = T extends object ? { @@ -22,7 +22,7 @@ type _Explode = T extends object key: `${K}${E['key'] extends '' ? '' : '.'}${E['key']}` value: E['value'] optional: E['key'] extends '' - ? {} extends Pick + ? object extends Pick ? true : false : E['optional'] @@ -42,6 +42,8 @@ type Collapse = { type FlattenObject = Collapse> export function flatten>(object: TObject) { + const result: Record = {} + function recurse(current: Record, prefix = '') { for (const key in current) { const value = current[key] @@ -55,7 +57,6 @@ export function flatten>(object: TObject } } - const result: Record = {} recurse(object) return result as FlattenObject } diff --git a/src/typeHelpers.ts b/src/typeHelpers.ts index 33d1b257..2aec0831 100644 --- a/src/typeHelpers.ts +++ b/src/typeHelpers.ts @@ -1,8 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + export type JSONValue = string | number | boolean | null | undefined | JSONObject | JSONArray type JSONObject = { [key: string]: JSONValue } type JSONArray = Array -// eslint-disable-next-line @typescript-eslint/no-explicit-any export type TAnyFunction = (...args: any[]) => TReturn export type Simplify = T extends unknown ? { [K in keyof T]: Simplify } : never diff --git a/src/unflatten/unflatten.ts b/src/unflatten/unflatten.ts index fc098b8a..f24ca7e2 100644 --- a/src/unflatten/unflatten.ts +++ b/src/unflatten/unflatten.ts @@ -22,6 +22,7 @@ type UnflattenObject> = { : never export function unflatten>(object: TObject) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {} for (const key in object) {