Skip to content

Commit

Permalink
feat(utils): construct, crush
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Jul 25, 2023
1 parent 31f009c commit b11e17d
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 97 deletions.
10 changes: 5 additions & 5 deletions src/types/__tests__/crush.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ import type TestSubject from '../crush'
import type EmptyObject from '../empty-object'
import type Fn from '../fn'
import type Get from '../get'
import type Invert from '../invert'
import type Nilable from '../nilable'
import type Nullable from '../nullable'
import type Objectify from '../objectify'

describe('unit-d:types/Crush', () => {
it('should equal Invert<T> if T is any', () => {
it('should equal Objectify<T> if T is any', () => {
// Arrange
type T = any

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Invert<T>>()
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Objectify<T>>()
})

it('should equal Invert<T> if T is never', () => {
it('should equal Objectify<T> if T is never', () => {
// Arrange
type T = never

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Invert<T>>()
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Objectify<T>>()
})

describe('T extends NIL', () => {
Expand Down
77 changes: 36 additions & 41 deletions src/types/crush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,66 @@

import type Dot from './dot'
import type Get from './get'
import type IfAnyOrNever from './if-any-or-never'
import type IfNegative from './if-negative'
import type IfNever from './if-never'
import type Indices from './indices'
import type Invert from './invert'
import type IsAnyOrNever from './is-any-or-never'
import type Join from './join'
import type Length from './length'
import type Nilable from './nilable'
import type NumberString from './number-string'
import type ObjectCurly from './object-curly'
import type Objectify from './objectify'
import type Path from './path'

/**
* Flattens `T` to a single dimension.
*
* Nested keys will be expressed using dot-notation.
* Nested keys will be expressed using dot-notation. Non-enumerable paths and
* top-level symbols are not preserved.
*
* @see {@linkcode Path}
*
* @todo examples
*
* @template T - Type to evaluate
*/
type Crush<T extends Nilable<object>> = IfAnyOrNever<
T,
Invert<ObjectCurly & T>,
T extends unknown
? Path<T, true> extends infer P extends NumberString
? Exclude<
P,
P extends NumberString
? Get<T, P> extends infer V
? V extends string
? Join<[P, Indices<V>], Dot>
: V extends readonly unknown[]
? number extends Length<V>
type Crush<T extends Nilable<object>> = IsAnyOrNever<T> extends true
? Objectify<T>
: T extends unknown
? Path<T, true> extends infer P extends NumberString
? Exclude<
P,
P extends NumberString
? Get<T, P> extends infer V
? V extends string
? Join<[P, Indices<V>], Dot>
: V extends readonly unknown[]
? Indices<V> extends infer I extends number
? number extends I
? never
: Indices<V> extends infer I extends number
? I extends number
? IfNegative<
I,
Join<[P, I, string] | [P, I], Dot> | P,
never
>
: never
: I extends unknown
? IfNegative<I, Join<[P, I, string] | [P, I], Dot> | P, never>
: never
: T extends readonly unknown[]
? number extends Length<T>
: never
: T extends readonly unknown[]
? Indices<T> extends infer I extends number
? number extends I
? never
: Indices<T> extends infer I extends number
? I extends number
? IfNegative<I, I | Join<[I, string] | [I], Dot>, never>
: never
: I extends unknown
? IfNegative<I, I | Join<[I, string] | [I], Dot>, never>
: never
: never
: never
: never
> extends infer Q extends P
? {
[K in Q as IfNever<Extract<Q, Join<[K, any], Dot>>, K, never>]: Get<
T,
K
>
}
: never
: never
> extends infer Q extends P
? {
[K in Q as IfNever<Extract<Q, Join<[K, any], Dot>>, K, never>]: Get<
T,
K
>
}
: never
: never
>
: never

export type { Crush as default }
93 changes: 42 additions & 51 deletions src/types/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import type BuiltIn from './built-in'
import type Dot from './dot'
import type Fn from './fn'
import type IfAny from './if-any'
import type IfIndexSignature from './if-index-signature'
import type IfNever from './if-never'
import type IfSymbol from './if-symbol'
Expand All @@ -20,72 +19,64 @@ import type Remap from './remap'
import type Stringify from './stringify'

/**
* Constructs a union of nested and top-level property paths.
* Construct a union of nested and top-level property paths.
*
* Nested and array-indexable paths are expressed using dot notation.
* Non-enumerable paths are included by default, but can be filtered out of the
* resulting union.
*
* Non-enumerable property paths can be filtered out by setting `E` to `true`.
* **Note**: TypeScript does not track enumerability. This type does its best to
* remove non-enumerable properties for {@linkcode BuiltIn} types only.
*
* **Note**: TypeScript does not track enumerability. Non-enumerable properties
* are removed for {@linkcode BuiltIn} types only.
* @see https://github.com/microsoft/TypeScript/issues/9726
*
* @todo document enumerability tracking constraints
* @todo examples
*
* @see https://github.com/microsoft/TypeScript/issues/9726
*
* @template T - Type to evaluate
* @template E - Enumerable property paths only
*/
type Path<T, E extends Nilable<boolean> = false> = Extract<
IfAny<
T,
keyof T,
T extends unknown
? Remap<T> extends infer U
? {
[H in keyof U as IfSymbol<
T extends unknown
? Remap<T> extends infer U
? {
[H in keyof U as IfSymbol<
H,
never,
IfIndexSignature<
T,
H,
never,
IfIndexSignature<
T,
H,
number extends H
? T extends string | readonly unknown[]
? number extends Length<T>
? H
: never
: H
: H,
E extends true
? T extends Readonly<BuiltIn>
? IfNever<Intersection<H, Keyof<BuiltIn>>, H, never>
: H
number extends H
? T extends string | readonly unknown[]
? number extends Length<T>
? H
: never
: H
: H,
E extends true
? T extends Readonly<BuiltIn>
? IfNever<Intersection<H, Keyof<BuiltIn>>, H, never>
: H
>
>]:
| H
| Stringify<H>
| (U[H] extends infer V
? V extends unknown
? [H, V] extends [number, string]
? T extends string
? never
: Join<[Stringify<H>, Path<V, E>], Dot>
: Readonly<Fn> extends V
: H
>
>]:
| H
| Stringify<H>
| (U[H] extends infer V
? V extends unknown
? [H, V] extends [number, string]
? T extends string
? never
: Join<[Stringify<H>, Path<V, E>], Dot>
: never
: never)
} extends infer X
? X[keyof X]
: never
: Readonly<Fn> extends V
? never
: Join<[Stringify<H>, Path<V, E>], Dot>
: never
: never)
} extends infer X
? X[keyof X]
: never
: never
>,
: never,
NumberString
> extends infer P extends NumberString
? P
: never
>

export type { Path as default }
18 changes: 18 additions & 0 deletions src/utils/__tests__/construct.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @file Type Tests - construct
* @module tutils/utils/tests/unit-d/construct
*/

import type Vehicle from '#fixtures/types/vehicle'
import type { Construct, Crush } from '#src/types'
import type testSubject from '../construct'

describe('unit-d:utils/construct', () => {
it('should return Construct<T>', () => {
// Arrange
type T = Crush<Vehicle & { driver: { nanoid: string } }>

// Expect
expectTypeOf<typeof testSubject<T>>().returns.toEqualTypeOf<Construct<T>>()
})
})
31 changes: 31 additions & 0 deletions src/utils/__tests__/construct.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @file Unit Tests - construct
* @module tutils/utils/tests/unit/construct
*/

import type Vehicle from '#fixtures/types/vehicle'
import testSubject from '../construct'

describe('unit:utils/construct', () => {
let obj: Vehicle & { 'driver.nanoid': string }

beforeAll(() => {
obj = {
'driver.nanoid': faker.string.nanoid(),
make: faker.vehicle.manufacturer(),
model: faker.vehicle.model(),
vin: faker.vehicle.vin(),
year: faker.date.past({ years: 3 }).getFullYear()
}
})

it('should return reconstructed object', () => {
expect(testSubject(obj)).to.deep.equal({
driver: { nanoid: obj['driver.nanoid'] },
make: obj.make,
model: obj.model,
vin: obj.vin,
year: obj.year
})
})
})
18 changes: 18 additions & 0 deletions src/utils/__tests__/crush.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @file Type Tests - crush
* @module tutils/utils/tests/unit-d/crush
*/

import type Book from '#fixtures/interfaces/book'
import type { Crush } from '#src/types'
import type testSubject from '../crush'

describe('unit-d:utils/crush', () => {
it('should return Crush<T>', () => {
// Arrange
type T = Book

// Expect
expectTypeOf<typeof testSubject<T>>().returns.toEqualTypeOf<Crush<T>>()
})
})
60 changes: 60 additions & 0 deletions src/utils/__tests__/crush.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @file Unit Tests - crush
* @module tutils/utils/tests/unit/crush
*/

import type Vehicle from '#fixtures/types/vehicle'
import VEHICLE from '#fixtures/vehicle'
import type { ObjectCurly } from '#src/types'
import testSubject from '../crush'
import isObjectPlain from '../is-object-plain'

describe('unit:utils/crush', () => {
let array: [Vehicle]
let pojo: Vehicle & { driver: { nanoid: string }; riders: { uuid: string }[] }

beforeAll(() => {
array = [VEHICLE]
pojo = {
...VEHICLE,
driver: { nanoid: faker.string.nanoid() },
riders: [{ uuid: faker.string.uuid() }, { uuid: faker.string.uuid() }]
}
})

it('should return one-dimensional plain object', () => {
// Arrange
const cases: [...Parameters<typeof testSubject>, ObjectCurly][] = [
[null, {}],
[undefined, {}],
[
array,
{
'0.make': array[0].make,
'0.model': array[0].model,
'0.vin': array[0].vin,
'0.year': array[0].year
}
],
[
pojo,
{
'driver.nanoid': pojo.driver.nanoid,
make: pojo.make,
model: pojo.model,
'riders.0.uuid': pojo.riders[0]!.uuid,
'riders.1.uuid': pojo.riders[1]!.uuid,
vin: pojo.vin,
year: pojo.year
}
]
]

// Act + Expect
cases.forEach(([obj, expected]) => {
expect(testSubject(obj))
.to.deep.equal(expected)
.and.satisfy(isObjectPlain)
})
})
})
Loading

0 comments on commit b11e17d

Please sign in to comment.