type-fest
Version:
A collection of essential TypeScript types
195 lines (161 loc) • 7.77 kB
TypeScript
import type {If} from './if.d.ts';
import type {NormalizedKeys} from './internal/object.d.ts';
import type {IfNotAnyOrNever, IsExactOptionalPropertyTypesEnabled, MapsSetsOrArrays} from './internal/type.d.ts';
import type {IsNever} from './is-never.d.ts';
import type {IsOptionalKeyOf} from './is-optional-key-of.d.ts';
import type {OmitIndexSignature} from './omit-index-signature.d.ts';
import type {PickIndexSignature} from './pick-index-signature.d.ts';
import type {RequiredKeysOf} from './required-keys-of.d.ts';
import type {Simplify} from './simplify.d.ts';
/**
Merge two object types into a new object type, where keys from the second override keys from the first.
@example
```ts
import type {ObjectMerge} from 'type-fest';
type PartialOverride = ObjectMerge<{foo: string; bar: string}, {foo: number; baz: number}>;
//=> {foo: number; baz: number; bar: string}
type CompleteOverride = ObjectMerge<{foo: string; bar: number}, {foo: number; bar: string; baz: boolean}>;
//=> {foo: number; bar: string; baz: boolean}
type NoOverride = ObjectMerge<{foo: string; bar: number}, {baz: boolean; qux: bigint}>;
//=> {baz: boolean; qux: bigint; foo: string; bar: number}
```
Use-cases:
Can be used to accurately type object spread and `Object.assign`. The built-in inference for these operations can sometimes be unsound, especially when index signatures are involved.
In the following example, both object spread and `Object.assign` produce a type that allows unsafe usage, whereas `ObjectMerge` produces a type that prevents this unsafe access.
@example
```ts
import type {ObjectMerge} from 'type-fest';
const left: {a: string} = {a: '1'};
const right: {[x: string]: number} = {a: 1};
const inferred = {...left, ...right};
//=> {a: string}
inferred.a.toUpperCase(); // No compile time error, but fails at runtime.
const objectAssign = Object.assign(left, right);
//=> {a: string} & {[x: string]: number}
objectAssign.a.toUpperCase(); // No compile time error, but fails at runtime.
declare const objectMerge: ObjectMerge<typeof left, typeof right>;
//=> {[x: string]: string | number; a: string | number}
// @ts-expect-error
objectMerge.a.toUpperCase(); // Correctly errors at compile time.
```
Can be used to merge generic type arguments.
In the following example, object spread without `ObjectMerge` produces an intersection type that is not particularly usable, whereas `ObjectMerge` produces a correctly merged and usable result.
@example
```ts
import type {ObjectMerge} from 'type-fest';
function withoutObjectMerge<T extends object, U extends object>(left: T, right: U) {
return {...left, ...right};
}
const result1 = withoutObjectMerge({a: 1}, {a: 'one'});
//=> {a: number} & {a: string}
const {a} = result1;
//=> never
function withObjectMerge<T extends object, U extends object>(left: T, right: U) {
return {...left, ...right} as unknown as ObjectMerge<T, U>;
}
const result2 = withObjectMerge({b: 1}, {b: 'one'});
//=> {b: string}
const {b} = result2;
//=> string
```
Note: If you want a simple merge where properties from the second object always override properties from the first object without considering runtime implications, refer to the {@link Merge} type.
@see {@link Merge}
@category Object
*/
export type ObjectMerge<First extends object, Second extends object> =
IfNotAnyOrNever<First, IfNotAnyOrNever<Second, First extends unknown // For distributing `First`
? Second extends unknown // For distributing `Second`
? First extends MapsSetsOrArrays
? unknown
: Second extends MapsSetsOrArrays
? unknown
: _ObjectMerge<
First,
Second,
NormalizedLiteralKeys<First>,
NormalizedLiteralKeys<Second>,
IsExactOptionalPropertyTypesEnabled extends true ? Required<First> : First,
IsExactOptionalPropertyTypesEnabled extends true ? Required<Second> : Second
>
: never // Should never happen
: never>, First & Second>; // Should never happen
type _ObjectMerge<
First extends object,
Second extends object,
NormalizedFirstLiteralKeys extends PropertyKey,
NormalizedSecondLiteralKeys extends PropertyKey,
NormalizedFirst extends object,
NormalizedSecond extends object,
> = Simplify<{
// Map over literal keys of `Second`, except those that are optional and also present in `First`.
-readonly [P in keyof Second as P extends NormalizedSecondLiteralKeys
? P extends NormalizedFirstLiteralKeys
? If<IsOptionalKeyOf<Second, P>, never, P>
: P
: never]:
| Second[P]
| (P extends NormalizedKeys<keyof PickIndexSignature<First>>
? If<IsOptionalKeyOf<Second, P>, First[NormalizedKeys<P> & keyof First], never>
: never)
} & {
// Map over literal keys of `First`, except those that are not present in `Second`.
-readonly [P in keyof First as P extends NormalizedFirstLiteralKeys
? P extends NormalizedSecondLiteralKeys
? never
: P
: never]:
| First[P]
// If there's a matching index signature in `Second`, then add the type for it as well,
// for example, in `ObjectMerge<{a: string}, {[x: string]: number}>`, `a` is of type `string | number`.
| (P extends NormalizedKeys<keyof Second>
? Second[NormalizedKeys<P> & keyof Second]
: never);
} & {
// Map over non-literal keys of `Second`.
-readonly [P in keyof Second as P extends NormalizedSecondLiteralKeys ? never : P]:
| Second[P]
// If there's a matching key in `First`, then add the type for it as well,
// for example, in `ObjectMerge<{a: number}, {[x: string]: string}>`,
// the resulting type is `{[x: string]: number | string; a: number | string}`.
// But, exclude keys from `First` that would surely get overwritten,
// for example, in `ObjectMerge<{a: number}, {[x: string]: string; a: string}>`,
// `a` from `First` would get overwritten by `a` from `Second`, so don't add type for it.
| (NormalizedKeys<P> & Exclude<keyof First, NormalizedKeys<RequiredKeysOf<OmitIndexSignature<Second>>>> extends infer NonOverwrittenKeysOfFirst
? If<IsNever<NonOverwrittenKeysOfFirst>, // This check is required because indexing with `never` doesn't always yield `never`, for example, `{[x: string]: number}[never]` results in `number`.
never,
NormalizedFirst[NonOverwrittenKeysOfFirst & keyof NormalizedFirst]>
: never); // Should never happen
} & {
// Map over non-literal keys of `First`.
-readonly [P in keyof First as P extends NormalizedFirstLiteralKeys ? never : P]:
| First[P]
| If<IsNever<NormalizedKeys<P> & keyof Second>, // This check is required because indexing with `never` doesn't always yield `never`, for example, `{[x: string]: number}[never]` results in `number`.
never,
NormalizedSecond[NormalizedKeys<P> & keyof NormalizedSecond]>;
} & {
// Handle optional keys of `Second` that are also present in `First`.
// Map over `First` instead of `Second` because the modifier is in accordance with `First`.
-readonly [P in keyof First as P extends NormalizedFirstLiteralKeys
? P extends NormalizedSecondLiteralKeys
? If<IsOptionalKeyOf<Second, NormalizedKeys<P> & keyof Second>, P, never>
: never
: never]:
| First[P]
| NormalizedSecond[NormalizedKeys<P> & keyof NormalizedSecond]
}>;
/**
Get literal keys of a type, including both string and number representations wherever applicable.
@example
```ts
type A = NormalizedLiteralKeys<{0: string; '1'?: number; foo: boolean}>;
//=> 0 | '0' | 1 | '1' | 'foo'
type B = NormalizedLiteralKeys<{[x: string]: string | number; 0: string; '1'?: number}>;
//=> 0 | '0' | 1 | '1'
type C = NormalizedLiteralKeys<{[x: string]: unknown}>;
//=> never
```
*/
type NormalizedLiteralKeys<Type> = Type extends unknown // For distributing `Type`
? NormalizedKeys<keyof OmitIndexSignature<Type>>
: never; // Should never happen
export {};