Skip to content

Commit

Permalink
feat(utils): overwriteWith, overwrite
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Jul 21, 2023
1 parent 89323a2 commit d4948a4
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 67 deletions.
1 change: 1 addition & 0 deletions .dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ booleanish
cefc
codecov
commitlintrc
customizer
dedupe
dequal
desegment
Expand Down
55 changes: 27 additions & 28 deletions src/types/__tests__/overwrite.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,53 @@
* @module tutils/types/tests/unit-d/Overwrite
*/

import type Author from '#fixtures/interfaces/author'
import type Book from '#fixtures/interfaces/book'
import type Person from '#fixtures/interfaces/person'
import type Vehicle from '#fixtures/types/vehicle'
import type Assign from '../assign'
import type EmptyArray from '../empty-array'
import type EmptyObject from '../empty-object'
import type Omit from '../omit'
import type OneOrMany from '../one-or-many'
import type { tag } from '../opaque'
import type TestSubject from '../overwrite'
import type Partial from '../partial'

describe('unit-d:types/Overwrite', () => {
it('should equal T if U is any', () => {
expectTypeOf<TestSubject<Vehicle, any>>().toEqualTypeOf<Vehicle>()
// Arrange
type T = Vehicle

// Expect
expectTypeOf<TestSubject<T, any>>().toEqualTypeOf<T>()
})

it('should equal T if U is never', () => {
expectTypeOf<TestSubject<Person, never>>().toEqualTypeOf<Person>()
// Arrange
type T = Vehicle

// Expect
expectTypeOf<TestSubject<T, never>>().toEqualTypeOf<T>()
})

describe('U extends ObjectCurly', () => {
type T = Author & { id?: string }
type T = Partial<Vehicle>

it('should assign mutual properties of U to T', () => {
// Arrange
type U = { foo: string; readonly id: string }
type Expect = Assign<T, Pick<U, 'id'>>
type U = { readonly vin: string; vrm: string }
type Expect = Assign<T, Omit<U, 'vrm'>>

// Expect
expectTypeOf<TestSubject<T, U>>().toEqualTypeOf<Expect>()
})

it('should equal T if Keyof<U> is never', () => {
it('should equal T if EmptyObject extends U', () => {
expectTypeOf<TestSubject<T>>().toEqualTypeOf<T>()
expectTypeOf<TestSubject<T, {}>>().toEqualTypeOf<T>()
})
})

describe('U extends readonly ObjectCurly[]', () => {
type T = Book & { [tag]?: 'book'; id?: string }
type T = Partial<Vehicle>

it('should equal T if U extends readonly EmptyObject[]', () => {
// Arrange
Expand All @@ -64,27 +69,21 @@ describe('unit-d:types/Overwrite', () => {
it('should assign mutual properties of U[i] to T', () => {
// Arrange
type U = [
{ readonly foo: string },
{ readonly [tag]?: 'book' },
{ readonly id?: string },
{ readonly publisher?: string },
{ readonly make?: Vehicle['make'] },
{ readonly model?: Vehicle['model'] },
{ readonly vin?: Vehicle['vin'] },
{ readonly vrm: string },
{ readonly year?: Vehicle['year'] },
any,
never,
{ readonly foo: string },
{ readonly [tag]: 'book' },
{ readonly id: string },
{ readonly publisher: string },
{ readonly readers: number }
{ readonly make: Vehicle['make'] },
{ readonly model: Vehicle['model'] },
{ readonly vin: Vehicle['vin'] },
{ readonly year: Vehicle['year'] }
]

// Expect
expectTypeOf<TestSubject<T, U>>().toEqualTypeOf<
Omit<T, typeof tag | 'publisher' | 'readers'> &
U[7] &
U[8] &
U[9] &
U[10]
>()
expectTypeOf<TestSubject<T, U>>().toEqualTypeOf<Readonly<Vehicle>>()
})

it('should equal T if U extends Readonly<EmptyArray>', () => {
Expand All @@ -96,8 +95,8 @@ describe('unit-d:types/Overwrite', () => {
describe('number extends Length<U>', () => {
it('should assign mutual properties of U[0] to T', () => {
// Arrange
type U = { foo: string; readonly id: string }[]
type Expect = Assign<T, Pick<U[0], 'id'>>
type U = { readonly vin: string; vrm: string }[]
type Expect = Assign<T, Omit<U[0], 'vrm'>>

// Expect
expectTypeOf<TestSubject<T, U>>().toEqualTypeOf<Expect>()
Expand Down
69 changes: 30 additions & 39 deletions src/types/overwrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,46 @@
*/

import type EmptyObject from './empty-object'
import type HasKeys from './has-keys'
import type HasKey from './has-key'
import type IsAnyOrNever from './is-any-or-never'
import type IsEqual from './is-equal'
import type ObjectCurly from './object-curly'
import type OmitIndexSignature from './omit-index-signature'
import type Objectify from './objectify'
import type OneOrMany from './one-or-many'

/**
* Assigns mutual properties of `H` to `T`.
* Assigns mutual properties of `T` and `U` to `T`.
*
* @internal
*
* @template T - Target object
* @template H - Source object
* @template U - Source object
*/
type Overwriter<T extends ObjectCurly, H> = IsAnyOrNever<H> extends true
type Overwriter<T extends ObjectCurly, U> = IsEqual<T, U> extends true
? T
: HasKeys<H> extends true
? IsEqual<T, H> extends true
? T
: H extends unknown
? {
[K in keyof ({
[K in keyof H as K extends keyof T ? K : never]: K
} & {
[K in keyof T as K extends keyof OmitIndexSignature<H> ? never : K]: K
})]: K extends keyof OmitIndexSignature<H>
? K extends keyof H
? H[K]
: never
: K extends keyof OmitIndexSignature<T>
? T[K]
: K extends keyof H
? H[K]
: K extends keyof T
? T[K]
: never
}
: T
: U extends ObjectCurly
? {
[K in keyof ({
[K in keyof T as HasKey<U, K> extends true ? never : K]: K
} & {
[K in keyof U as HasKey<T, K> extends true ? K : never]: K
})]: HasKey<U, K> extends true
? U[K & keyof U]
: HasKey<T, K> extends true
? T[K & keyof T]
: never
} extends infer X extends ObjectCurly
? X
: never
: T

/**
* Assigns properties from one or more source objects to target object `T` for
* all mutual properties in `T`.
*
* A mutual property is a property that is contained in both target object `T`
* and a source object.
* all mutual properties in `T`. A mutual property is a property that exists on
* both `T` and a source object.
*
* Source objects are applied from left to right.
* Source objects are applied from left to right. Subsequent sources overwrite
* property assignments of previous sources.
*
* @todo examples
*
Expand All @@ -62,15 +53,15 @@ type Overwriter<T extends ObjectCurly, H> = IsAnyOrNever<H> extends true
type Overwrite<
T extends ObjectCurly,
U extends OneOrMany<ObjectCurly> = EmptyObject
> = T extends ObjectCurly
? IsAnyOrNever<U> extends true
? T
: U extends ObjectCurly
? Overwriter<T, U>
> = IsAnyOrNever<U> extends true
? Overwriter<T, Objectify<U>>
: T extends unknown
? U extends ObjectCurly
? Overwriter<T, Objectify<U>>
: U extends readonly ObjectCurly[]
? U extends readonly [infer H, ...infer R extends ObjectCurly[]]
? Overwrite<Overwriter<T, H>, R>
: Overwriter<T, U[0]>
? Overwrite<Overwriter<T, Objectify<H>>, R>
: Overwriter<T, Objectify<U[0]>>
: never
: never

Expand Down
20 changes: 20 additions & 0 deletions src/utils/__tests__/overwrite-with.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @file Type Tests - overwriteWith
* @module tutils/utils/tests/unit-d/overwriteWith
*/

import type Vehicle from '#fixtures/types/vehicle'
import type { Overwrite } from '#src/types'
import type testSubject from '../overwrite-with'

describe('unit-d:utils/overwriteWith', () => {
it('should return Overwrite<T, U>', () => {
// Arrange
type T = Vehicle
type U = [{ year: `${number}${number}${number}${number}` }]
type Expect = Overwrite<T, U>

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

import type Vehicle from '#fixtures/types/vehicle'
import isUndefined from '../is-undefined'
import type { OverwriteCustomizer } from '../overwrite-with'
import testSubject from '../overwrite-with'

describe('unit:utils/overwriteWith', () => {
let customizer: OverwriteCustomizer

beforeAll(() => {
customizer = (outgoing: any, incoming: any): any => {
return isUndefined(incoming) ? outgoing : incoming
}
})

it('should return overwrite result', () => {
// Arrange
const base: { vin: Vehicle['vin'] } = { vin: '' }
const s1: { vrm: string } = { vrm: faker.vehicle.vrm() }
const s2: { vin: Vehicle['vin'] } = { vin: faker.vehicle.vin() }

// Act + Expect
expect(testSubject(customizer, base, s1, s2, { vin: undefined }))
.to.eql(s2)
.but.not.equal(base)
})
})
20 changes: 20 additions & 0 deletions src/utils/__tests__/overwrite.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @file Type Tests - overwrite
* @module tutils/utils/tests/unit-d/overwrite
*/

import type Vehicle from '#fixtures/types/vehicle'
import type { Overwrite } from '#src/types'
import type testSubject from '../overwrite'

describe('unit-d:utils/overwrite', () => {
it('should return Overwrite<T, U>', () => {
// Arrange
type T = Vehicle
type U = [{ year: `${number}${number}${number}${number}` }]
type Expect = Overwrite<T, U>

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

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

describe('unit:utils/overwrite', () => {
it('should return overwrite result', () => {
// Arrange
const base: { vin: Vehicle['vin'] } = { vin: '' }
const s1: { vrm: string } = { vrm: faker.vehicle.vrm() }
const s2: { vin: Vehicle['vin'] } = { vin: faker.vehicle.vin() }

// Act + Expect
expect(testSubject(base, s1, s2)).to.eql(s2).but.not.equal(base)
})
})
5 changes: 5 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export { default as keys } from './keys'
export type { default as KeysOptions } from './keys.options'
export { default as lowercase } from './lowercase'
export { default as noop } from './noop'
export { default as overwrite } from './overwrite'
export {
default as overwriteWith,
type OverwriteCustomizer
} from './overwrite-with'
export { default as properties, type Properties } from './properties'
export type { default as PropertiesOptions } from './properties.options'
export { default as pull } from './pull'
Expand Down
64 changes: 64 additions & 0 deletions src/utils/overwrite-with.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @file Utilities - overwriteWith
* @module tutils/utils/overwriteWith
*/

import type { Fn, ObjectCurly, Overwrite } from '#src/types'
import cast from './cast'
import clone from './clone'
import define from './define'
import descriptor from './descriptor'
import hasOwn from './has-own'
import properties from './properties'

/**
* Function used to customize assigned values.
*
* @param {unknown} outgoing - Outgoing property value
* @param {unknown} incoming - Incoming property value from source object
* @param {string | symbol} key - Current property key
* @return {unknown} New property value
*/
type OverwriteCustomizer = Fn<[unknown, unknown, string | symbol], unknown>

/**
* Assigns own properties from one or more `source` objects to a destination
* object for all mutual properties in `base`. A mutual property is a property
* that exists on both `T` and a source object.
*
* A `customizer` is used to produce assigned values.
*
* Source objects are applied from left to right. Subsequent sources overwrite
* property assignments of previous sources.
*
* @see {@linkcode Overwrite}
* @see {@linkcode OverwriteCustomizer}
*
* @todo examples
*
* @template T - Base object
* @template U - Source object array
*
* @param {OverwriteCustomizer} customizer - Assigned value factory
* @param {T} base - Base object
* @param {U} source - Source object(s)
* @return {Overwrite<T, U>} Overwrite result
*/
const overwriteWith = <T extends ObjectCurly, U extends readonly ObjectCurly[]>(
customizer: OverwriteCustomizer,
base: T,
...source: U
): Overwrite<T, U> => {
return source.reduce<Overwrite<T, U>>((acc, src) => {
return properties(src).reduce<Overwrite<T, U>>((acc, key) => {
return hasOwn(acc, key)
? define(acc, key, {
...descriptor(src, key),
value: customizer(acc[key], src[key], key)
})
: acc
}, acc)
}, cast(clone(base)))
}

export { overwriteWith as default, type OverwriteCustomizer }
Loading

0 comments on commit d4948a4

Please sign in to comment.