-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #940 from devoxa/flatten-unflatten
Add flatten & unflatten functions
- Loading branch information
Showing
7 changed files
with
197 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/** | ||
* ### 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: unknown; optional: boolean } | ||
|
||
type Explode<T> = _Explode<T extends readonly unknown[] ? { '0': T[number] } : T> | ||
|
||
type _Explode<T> = T extends object | ||
? { | ||
[K in keyof T]-?: K extends string | ||
? Explode<T[K]> extends infer E | ||
? E extends Entry | ||
? { | ||
key: `${K}${E['key'] extends '' ? '' : '.'}${E['key']}` | ||
value: E['value'] | ||
optional: E['key'] extends '' | ||
? object extends Pick<T, K> | ||
? true | ||
: false | ||
: E['optional'] | ||
} | ||
: never | ||
: never | ||
: never | ||
}[keyof T] | ||
: { key: ''; value: T; optional: false } | ||
|
||
type Collapse<T extends Entry> = { | ||
[E in Extract<T, { optional: false }> as E['key']]: E['value'] | ||
} & Partial<{ [E in Extract<T, { optional: true }> as E['key']]: E['value'] }> extends infer O | ||
? { [K in keyof O]: O[K] } | ||
: never | ||
|
||
type FlattenObject<T> = Collapse<Explode<T>> | ||
|
||
export function flatten<TObject extends Record<string, unknown>>(object: TObject) { | ||
const result: Record<string, unknown> = {} | ||
|
||
function recurse(current: Record<string, unknown>, 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<string, unknown>, nextKey) | ||
} else { | ||
result[nextKey] = value | ||
} | ||
} | ||
} | ||
|
||
recurse(object) | ||
return result as FlattenObject<TObject> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,21 @@ | ||
/* 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<JSONValue> | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export type TAnyFunction<TReturn> = (...args: any[]) => TReturn | ||
|
||
export type Simplify<T> = T extends unknown ? { [K in keyof T]: Simplify<T[K]> } : never | ||
|
||
export type RecursiveUnionToIntersection<TObjectWithUnions> = { | ||
[TKey in keyof TObjectWithUnions]: TObjectWithUnions[TKey] extends object | ||
? RecursiveUnionToIntersection<UnionToIntersection<TObjectWithUnions[TKey]>> | ||
: TObjectWithUnions[TKey] | ||
} | ||
|
||
export type UnionToIntersection<TUnion> = ( | ||
TUnion extends any ? (k: TUnion) => void : never | ||
) extends (k: infer TIntersection) => void | ||
? TIntersection | ||
: never |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
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 UnflattenObject<TObject extends Record<string, unknown>> = { | ||
[TKey in keyof TObject as TKey extends `${infer TKeyPrefix}.${string}` | ||
? TKeyPrefix | ||
: TKey]: TKey extends `${string}.${infer TKeySuffix}` | ||
? UnflattenObject<{ [key in TKeySuffix]: TObject[TKey] }> | ||
: TObject[TKey] | ||
} extends infer TResult | ||
? { [TResultKey in keyof TResult]: TResult[TResultKey] } | ||
: never | ||
|
||
export function unflatten<TObject extends Record<string, unknown>>(object: TObject) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
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<RecursiveUnionToIntersection<UnflattenObject<TObject>>> | ||
} |