Skip to content

Commit

Permalink
feat(types): Merge, MergeDefaults
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed May 24, 2023
1 parent ca855cb commit ac95845
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 9 deletions.
4 changes: 2 additions & 2 deletions __tests__/reporters/notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @module tests/reporters/Notifier
*/

import type { OneOrMany } from '#src'
import { isArray, type OneOrMany } from '#src'
import notifier from 'node-notifier'
import type NotificationCenter from 'node-notifier/notifiers/notificationcenter'
import { performance } from 'node:perf_hooks'
Expand Down Expand Up @@ -142,7 +142,7 @@ class Notifier implements Reporter {
protected tests(tasks: OneOrMany<Task> = []): Test[] {
const { mode } = this.ctx

return (Array.isArray(tasks) ? tasks : [tasks]).flatMap(task => {
return (isArray<Task>(tasks) ? tasks : [tasks]).flatMap(task => {
const { type } = task

return mode === 'typecheck' && type === 'suite' && task.tasks.length === 0
Expand Down
40 changes: 40 additions & 0 deletions src/types/__tests__/merge-defaults.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @file Type Tests - MergeDefaults
* @module tutils/types/tests/unit-d/MergeDefaults
*/

import type Author from '#fixtures/author.interface'
import type EmptyArray from '../empty-array'
import type Merge from '../merge'
import type TestSubject from '../merge-defaults'

describe('unit-d:types/MergeDefaults', () => {
it('should equal T if U extends EmptyArray', () => {
expectTypeOf<TestSubject<Author, EmptyArray>>().toEqualTypeOf<Author>()
})

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

it('should merge defaults into T if U extends ObjectAny', () => {
// Arrange
type U = { email: Lowercase<string>; first_name?: string }
type Expected = Merge<Author, Omit<U, 'first_name'>>

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

it('should merge defaults into T if U extends readonly ObjectAny[]', () => {
// Arrange
type U1 = { display_name: string; first_name?: string }[]
type U2 = [{ display_name: string }, { first_name?: string }]
type E1 = Merge<Author, Omit<U1[0], 'first_name'>>
type E2 = Merge<Author, U2[0]>

// Expect
expectTypeOf<TestSubject<Author, U1>>().toEqualTypeOf<E1>()
expectTypeOf<TestSubject<Author, U2>>().toEqualTypeOf<E2>()
})
})
24 changes: 24 additions & 0 deletions src/types/__tests__/merge.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @file Type Tests - Merge
* @module tutils/types/tests/unit-d/Merge
*/

import type Author from '#fixtures/author.interface'
import type TestSubject from '../merge'
import type Simplify from '../simplify'

describe('unit-d:types/Merge', () => {
it('should equal Simplify<Omit<T, keyof U> & U>', () => {
// Arrange
type T = Author
type U = { display_name: string }
type Expected = Simplify<Omit<T, keyof U> & U>

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

it('should equal T if U extends EmptyObject', () => {
expectTypeOf<TestSubject<Author>>().toEqualTypeOf<Author>()
})
})
12 changes: 8 additions & 4 deletions src/types/__tests__/one-or-many.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import type TestSubject from '../one-or-many'
describe('unit-d:types/OneOrMany', () => {
type T = string

it('should extract T', () => {
expectTypeOf<TestSubject<T>>().extract<T>().toEqualTypeOf<T>()
it('should match T', () => {
expectTypeOf<T>().toMatchTypeOf<TestSubject<T>>()
})

it('should extract T[]', () => {
expectTypeOf<TestSubject<T>>().extract<T[]>().toEqualTypeOf<T[]>()
it('should match T[]', () => {
expectTypeOf<T[]>().toMatchTypeOf<TestSubject<T>>()
})

it('should match readonly T[]', () => {
expectTypeOf<readonly T[]>().toMatchTypeOf<TestSubject<T>>()
})
})
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export type { default as Keys } from './keys'
export type { default as OptionalKeys } from './keys-optional'
export type { default as RequiredKeys } from './keys-required'
export type { default as LiteralUnion } from './literal-union'
export type { default as Merge } from './merge'
export type { default as MergeDefaults } from './merge-defaults'
export type { default as NIL } from './nil'
export type { default as Nilable } from './nilable'
export type { default as Nullable } from './nullable'
Expand Down
53 changes: 53 additions & 0 deletions src/types/merge-defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @file Type Definitions - MergeDefaults
* @module tutils/types/MergeDefaults
*/

import type EmptyArray from './empty-array'
import type EmptyObject from './empty-object'
import type Head from './head'
import type IfNever from './if-never'
import type Merge from './merge'
import type ObjectAny from './object-any'
import type OneOrMany from './one-or-many'

/**
* Assigns properties from one or more source objects to target object `T` for
* all optional properties in `T`.
*
* Source objects are applied from left to right. Once a property is set on `T`,
* additional values of the same property are ignored if the property is no
* longer optional.
*
* @template T - Target object
* @template U - Source object or source object array
*/
type MergeDefaults<
T extends ObjectAny,
U extends OneOrMany<Partial<T>> = EmptyObject
> = U extends EmptyArray | EmptyObject
? T
: U extends Partial<T>
? MergeDefaults<T, [U]>
: U extends [infer H, ...infer Rest extends readonly ObjectAny[]]
? Merge<
T,
{
[K in keyof H & keyof T]: T[K & keyof T] extends infer V
? IfNever<
Extract<V, undefined>,
V,
Exclude<V, H[K & keyof H] | undefined> | H[K & keyof H]
>
: never
}
> extends infer V extends ObjectAny
? Rest extends readonly Partial<V>[]
? MergeDefaults<V, Rest>
: never
: never
: Head<U> extends infer S extends Partial<T>
? MergeDefaults<T, [S]>
: never

export type { MergeDefaults as default }
23 changes: 23 additions & 0 deletions src/types/merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @file Type Definitions - Merge
* @module tutils/types/Merge
*/

import type EmptyObject from './empty-object'
import type ObjectAny from './object-any'
import type Simplify from './simplify'

/**
* Merges two types into one.
*
* Keys of `U` override `T`.
*
* @template T - Target object
* @template U - Source object
*/
type Merge<
T extends ObjectAny,
U extends ObjectAny = EmptyObject
> = U extends EmptyObject ? T : Simplify<Omit<T, keyof U> & U>

export type { Merge as default }
6 changes: 3 additions & 3 deletions src/types/one-or-many.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
*/

/**
* Restricts a type to type `T` or an array of `T` values.
* Constructs a union of `T` and an array of `T` values.
*
* @template T - Value type
* @template T - Type to evaluate
*/
type OneOrMany<T> = T | T[]
type OneOrMany<T> = T | T[] | readonly T[]

export type { OneOrMany as default }

0 comments on commit ac95845

Please sign in to comment.