Skip to content

Commit

Permalink
fix(types): make assign return type more accurate + add Assign type
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson committed Aug 15, 2024
1 parent bc612fd commit dcb5545
Show file tree
Hide file tree
Showing 4 changed files with 539 additions and 35 deletions.
134 changes: 128 additions & 6 deletions src/object/assign.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { isPlainObject } from 'radashi'
import {
isPlainObject,
type BoxedPrimitive,
type BuiltInType,
type CustomClass,
type IsExactType,
type OptionalKeys,
type RequiredKeys,
} from 'radashi'

/**
* Create a copy of the first object, and then merge the second object
Expand All @@ -14,12 +22,12 @@ import { isPlainObject } from 'radashi'
* // => { a: 1, b: 2, c: 3, p: { a: 4, b: 5 } }
* ```
*/
export function assign<X extends Record<string | symbol | number, any>>(
initial: X,
override: X,
): X {
export function assign<
TInitial extends Record<keyof any, any>,
TOverride extends Record<keyof any, any>,
>(initial: TInitial, override: TOverride): Assign<TInitial, TOverride> {
if (!initial || !override) {
return initial ?? override ?? {}
return (initial ?? override ?? {}) as any
}
const proto = Object.getPrototypeOf(initial)
const merged = proto
Expand All @@ -33,3 +41,117 @@ export function assign<X extends Record<string | symbol | number, any>>(
}
return merged
}

/**
* The return type for `assign`.
*
* It recursively merges object types that are not native objects. The
* root objects are always merged.
*
* @see https://radashi-org.github.io/reference/object/assign
*/
export type Assign<
TInitial extends object,
TOverride extends object,
> = TInitial extends any
? TOverride extends any
? SimplifyMutable<
Omit<TInitial, keyof TOverride> &
Omit<TOverride, keyof TInitial> &
(Pick<
TInitial,
keyof TInitial & keyof TOverride
> extends infer TConflictInitial
? Pick<
TOverride,
keyof TInitial & keyof TOverride
> extends infer TConflictOverride
? {
[K in RequiredKeys<TConflictOverride>]: AssignDeep<
TConflictInitial[K & keyof TConflictInitial],
TConflictOverride[K]
>
} & {
[K in RequiredKeys<TConflictInitial> &
OptionalKeys<TConflictOverride>]: AssignDeep<
TConflictInitial[K],
TConflictOverride[K],
true
>
} & {
[K in OptionalKeys<TConflictInitial> &
OptionalKeys<TConflictOverride>]?: AssignDeep<
TConflictInitial[K],
TConflictOverride[K],
true
>
}
: unknown
: unknown)
>
: never
: never

/**
* Mimic the `Simplify` type and also remove `readonly` modifiers.
*/
type SimplifyMutable<T> = {} & {
-readonly [P in keyof T]: T[P]
}

/**
* This represents a value that should only be replaced if it exists
* as an initial value; never deeply assigned into.
*/
type AtomicValue = BuiltInType | CustomClass | BoxedPrimitive

/**
* Handle mixed types when merging nested plain objects.
*
* For example, if the type `TOverride` includes both `string` and `{ n:
* number }` in a union, `AssignDeep` will treat `string` as
* unmergeable and `{ n: number }` as mergeable.
*/
type AssignDeep<TInitial, TOverride, IsOptional = false> =
| never // <-- ignore me!
/**
* When a native type is found in TInitial, it will only exist in
* the result type if the override is optional.
*/
| (TInitial extends AtomicValue
? IsOptional extends true
? TInitial
: never
: never)
/**
* When a native type is found in TOverride, it will always exists
* in the result type.
*/
| (TOverride extends AtomicValue ? TOverride : never)
/**
* Deep assignment is handled in this branch.
*
* 1. Exclude any native types from TInitial and TOverride
* 2. If a non-native object type is not found in TInitial, simply
* replace TInitial (or use "A | B" if the override is optional)
* 3. For each non-native object type in TOverride, deep assign to
* every non-native object in TInitial
* 4. For each non-object type in TOverride, simply replace TInitial
* (or use "A | B" if the override is optional)
*/
| (Exclude<TOverride, AtomicValue> extends infer TOverride // 1.
? Exclude<TInitial, Exclude<AtomicValue, void>> extends infer TInitial
? [Extract<TInitial, object>] extends [never] // 2.
? TOverride | (IsOptional extends true ? TInitial : never)
: TInitial extends object
? TOverride extends object
? IsExactType<TOverride, TInitial> extends true
? TOverride
: Assign<TInitial, TOverride> // 3.
: // 4.
TOverride | (IsOptional extends true ? TInitial : never)
:
| Extract<TOverride, object>
| (IsOptional extends true ? TInitial : never)
: never
: never)
Loading

0 comments on commit dcb5545

Please sign in to comment.