UNPKG

type-fest

Version:

A collection of essential TypeScript types

471 lines (405 loc) 15.8 kB
import type {ConditionalSimplifyDeep} from './conditional-simplify'; import type {OmitIndexSignature} from './omit-index-signature'; import type {PickIndexSignature} from './pick-index-signature'; import type {EnforceOptional} from './enforce-optional'; import type {Merge} from './merge'; import type { ArrayTail, FirstArrayElement, IsBothExtends, NonEmptyTuple, UnknownArrayOrTuple, UnknownRecord, } from './internal'; /** Deeply simplifies an object excluding iterables and functions. Used internally to improve the UX and accept both interfaces and type aliases as inputs. */ type SimplifyDeep<Type> = ConditionalSimplifyDeep<Type, Function | Iterable<unknown>, object>; /** Try to merge two record properties or return the source property value, preserving `undefined` properties values in both cases. */ type MergeDeepRecordProperty< Destination, Source, Options extends MergeDeepInternalOptions, > = undefined extends Source ? MergeDeepOrReturn<Source, Exclude<Destination, undefined>, Exclude<Source, undefined>, Options> | undefined : MergeDeepOrReturn<Source, Destination, Source, Options>; /** Walk through the union of the keys of the two objects and test in which object the properties are defined. - If the source does not contain the key, the value of the destination is returned. - If the source contains the key and the destination does not contain the key, the value of the source is returned. - If both contain the key, try to merge according to the chosen {@link MergeDeepOptions options} or return the source if unable to merge. */ type DoMergeDeepRecord< Destination extends UnknownRecord, Source extends UnknownRecord, Options extends MergeDeepInternalOptions, > = EnforceOptional<{ [Key in keyof Destination | keyof Source]: Key extends keyof Source ? Key extends keyof Destination ? MergeDeepRecordProperty<Destination[Key], Source[Key], Options> : Source[Key] : Key extends keyof Destination ? Destination[Key] : never; }>; /** Wrapper around {@link DoMergeDeepRecord} which preserves index signatures. */ type MergeDeepRecord< Destination extends UnknownRecord, Source extends UnknownRecord, Options extends MergeDeepInternalOptions, > = DoMergeDeepRecord<OmitIndexSignature<Destination>, OmitIndexSignature<Source>, Options> & Merge<PickIndexSignature<Destination>, PickIndexSignature<Source>>; /** Pick the rest type. @example ``` type Rest1 = PickRestType<[]>; // => [] type Rest2 = PickRestType<[string]>; // => [] type Rest3 = PickRestType<[...number[]]>; // => number[] type Rest4 = PickRestType<[string, ...number[]]>; // => number[] type Rest5 = PickRestType<string[]>; // => string[] ``` */ type PickRestType<Type extends UnknownArrayOrTuple> = number extends Type['length'] ? ArrayTail<Type> extends [] ? Type : PickRestType<ArrayTail<Type>> : []; /** Omit the rest type. @example ``` type Tuple1 = OmitRestType<[]>; // => [] type Tuple2 = OmitRestType<[string]>; // => [string] type Tuple3 = OmitRestType<[...number[]]>; // => [] type Tuple4 = OmitRestType<[string, ...number[]]>; // => [string] type Tuple5 = OmitRestType<[string, boolean[], ...number[]]>; // => [string, boolean[]] type Tuple6 = OmitRestType<string[]>; // => [] ``` */ type OmitRestType<Type extends UnknownArrayOrTuple, Result extends UnknownArrayOrTuple = []> = number extends Type['length'] ? ArrayTail<Type> extends [] ? Result : OmitRestType<ArrayTail<Type>, [...Result, FirstArrayElement<Type>]> : Type; // Utility to avoid picking two times the type. type TypeNumberOrType<Type extends UnknownArrayOrTuple> = Type[number] extends never ? Type : Type[number]; // Pick the rest type (array) and try to get the intrinsic type or return the provided type. type PickRestTypeFlat<Type extends UnknownArrayOrTuple> = TypeNumberOrType<PickRestType<Type>>; /** Try to merge two array/tuple elements or return the source element if the end of the destination is reached or vis-versa. */ type MergeDeepArrayOrTupleElements< Destination, Source, Options extends MergeDeepInternalOptions, > = Source extends [] ? Destination : Destination extends [] ? Source : MergeDeepOrReturn<Source, Destination, Source, Options>; /** Merge two tuples recursively. */ type DoMergeDeepTupleAndTupleRecursive< Destination extends UnknownArrayOrTuple, Source extends UnknownArrayOrTuple, DestinationRestType, SourceRestType, Options extends MergeDeepInternalOptions, > = Destination extends [] ? Source extends [] ? [] : MergeArrayTypeAndTuple<DestinationRestType, Source, Options> : Source extends [] ? MergeTupleAndArrayType<Destination, SourceRestType, Options> : [ MergeDeepArrayOrTupleElements<FirstArrayElement<Destination>, FirstArrayElement<Source>, Options>, ...DoMergeDeepTupleAndTupleRecursive<ArrayTail<Destination>, ArrayTail<Source>, DestinationRestType, SourceRestType, Options>, ]; /** Merge two tuples recursively taking into account a possible rest element. */ type MergeDeepTupleAndTupleRecursive< Destination extends UnknownArrayOrTuple, Source extends UnknownArrayOrTuple, Options extends MergeDeepInternalOptions, > = [ ...DoMergeDeepTupleAndTupleRecursive<OmitRestType<Destination>, OmitRestType<Source>, PickRestTypeFlat<Destination>, PickRestTypeFlat<Source>, Options>, ...MergeDeepArrayOrTupleElements<PickRestType<Destination>, PickRestType<Source>, Options>, ]; /** Merge an array type with a tuple recursively. */ type MergeTupleAndArrayType< Tuple extends UnknownArrayOrTuple, ArrayType, Options extends MergeDeepInternalOptions, > = Tuple extends [] ? Tuple : [ MergeDeepArrayOrTupleElements<FirstArrayElement<Tuple>, ArrayType, Options>, ...MergeTupleAndArrayType<ArrayTail<Tuple>, ArrayType, Options>, ]; /** Merge an array into a tuple recursively taking into account a possible rest element. */ type MergeDeepTupleAndArrayRecursive< Destination extends UnknownArrayOrTuple, Source extends UnknownArrayOrTuple, Options extends MergeDeepInternalOptions, > = [ ...MergeTupleAndArrayType<OmitRestType<Destination>, Source[number], Options>, ...MergeDeepArrayOrTupleElements<PickRestType<Destination>, PickRestType<Source>, Options>, ]; /** Merge a tuple with an array type recursively. */ type MergeArrayTypeAndTuple< ArrayType, Tuple extends UnknownArrayOrTuple, Options extends MergeDeepInternalOptions, > = Tuple extends [] ? Tuple : [ MergeDeepArrayOrTupleElements<ArrayType, FirstArrayElement<Tuple>, Options>, ...MergeArrayTypeAndTuple<ArrayType, ArrayTail<Tuple>, Options>, ]; /** Merge a tuple into an array recursively taking into account a possible rest element. */ type MergeDeepArrayAndTupleRecursive< Destination extends UnknownArrayOrTuple, Source extends UnknownArrayOrTuple, Options extends MergeDeepInternalOptions, > = [ ...MergeArrayTypeAndTuple<Destination[number], OmitRestType<Source>, Options>, ...MergeDeepArrayOrTupleElements<PickRestType<Destination>, PickRestType<Source>, Options>, ]; /** Merge mode for array/tuple elements. */ type ArrayMergeMode = 'spread' | 'replace'; /** Test if it should spread top-level arrays. */ type ShouldSpread<Options extends MergeDeepInternalOptions> = Options['spreadTopLevelArrays'] extends false ? Options['arrayMergeMode'] extends 'spread' ? true : false : true; /** Merge two arrays/tuples according to the chosen {@link MergeDeepOptions.arrayMergeMode arrayMergeMode} option. */ type DoMergeArrayOrTuple< Destination extends UnknownArrayOrTuple, Source extends UnknownArrayOrTuple, Options extends MergeDeepInternalOptions, > = ShouldSpread<Options> extends true ? Array<Exclude<Destination, undefined>[number] | Exclude<Source, undefined>[number]> : Source; // 'replace' /** Merge two arrays recursively. If the two arrays are multi-level, we merge deeply, otherwise we merge the first level only. Note: The `[number]` accessor is used to test the type of the second level. */ type MergeDeepArrayRecursive< Destination extends UnknownArrayOrTuple, Source extends UnknownArrayOrTuple, Options extends MergeDeepInternalOptions, > = Destination[number] extends UnknownArrayOrTuple ? Source[number] extends UnknownArrayOrTuple ? Array<MergeDeepArrayOrTupleRecursive<Destination[number], Source[number], Options>> : DoMergeArrayOrTuple<Destination, Source, Options> : Destination[number] extends UnknownRecord ? Source[number] extends UnknownRecord ? Array<SimplifyDeep<MergeDeepRecord<Destination[number], Source[number], Options>>> : DoMergeArrayOrTuple<Destination, Source, Options> : DoMergeArrayOrTuple<Destination, Source, Options>; /** Merge two array/tuple recursively by selecting one of the four strategies according to the type of inputs. - tuple/tuple - tuple/array - array/tuple - array/array */ type MergeDeepArrayOrTupleRecursive< Destination extends UnknownArrayOrTuple, Source extends UnknownArrayOrTuple, Options extends MergeDeepInternalOptions, > = IsBothExtends<NonEmptyTuple, Destination, Source> extends true ? MergeDeepTupleAndTupleRecursive<Destination, Source, Options> : Destination extends NonEmptyTuple ? MergeDeepTupleAndArrayRecursive<Destination, Source, Options> : Source extends NonEmptyTuple ? MergeDeepArrayAndTupleRecursive<Destination, Source, Options> : MergeDeepArrayRecursive<Destination, Source, Options>; /** Merge two array/tuple according to {@link MergeDeepOptions.recurseIntoArrays recurseIntoArrays} option. */ type MergeDeepArrayOrTuple< Destination extends UnknownArrayOrTuple, Source extends UnknownArrayOrTuple, Options extends MergeDeepInternalOptions, > = Options['recurseIntoArrays'] extends true ? MergeDeepArrayOrTupleRecursive<Destination, Source, Options> : DoMergeArrayOrTuple<Destination, Source, Options>; /** Try to merge two objects or two arrays/tuples recursively into a new type or return the default value. */ type MergeDeepOrReturn< DefaultType, Destination, Source, Options extends MergeDeepInternalOptions, > = SimplifyDeep<[undefined] extends [Destination | Source] ? DefaultType : Destination extends UnknownRecord ? Source extends UnknownRecord ? MergeDeepRecord<Destination, Source, Options> : DefaultType : Destination extends UnknownArrayOrTuple ? Source extends UnknownArrayOrTuple ? MergeDeepArrayOrTuple<Destination, Source, Merge<Options, {spreadTopLevelArrays: false}>> : DefaultType : DefaultType>; /** MergeDeep options. @see {@link MergeDeep} */ export type MergeDeepOptions = { /** Merge mode for array and tuple. When we walk through the properties of the objects and the same key is found and both are array or tuple, a merge mode must be chosen: - `replace`: Replaces the destination value by the source value. This is the default mode. - `spread`: Spreads the destination and the source values. See {@link MergeDeep} for usages and examples. Note: Top-level arrays and tuples are always spread. @default 'spread' */ arrayMergeMode?: ArrayMergeMode; /** Whether to affect the individual elements of arrays and tuples. If this option is set to `true` the following rules are applied: - If the source does not contain the key, the value of the destination is returned. - If the source contains the key and the destination does not contain the key, the value of the source is returned. - If both contain the key, try to merge according to the chosen {@link MergeDeepOptions.arrayMergeMode arrayMergeMode} or return the source if unable to merge. @default false */ recurseIntoArrays?: boolean; }; /** Internal options. */ type MergeDeepInternalOptions = Merge<MergeDeepOptions, {spreadTopLevelArrays?: boolean}>; /** Merge default and internal options with user provided options. */ type DefaultMergeDeepOptions<Options extends MergeDeepOptions> = Merge<{ arrayMergeMode: 'replace'; recurseIntoArrays: false; spreadTopLevelArrays: true; }, Options>; /** This utility selects the correct entry point with the corresponding default options. This avoids re-merging the options at each iteration. */ type MergeDeepWithDefaultOptions<Destination, Source, Options extends MergeDeepOptions> = SimplifyDeep< [undefined] extends [Destination | Source] ? never : Destination extends UnknownRecord ? Source extends UnknownRecord ? MergeDeepRecord<Destination, Source, DefaultMergeDeepOptions<Options>> : never : Destination extends UnknownArrayOrTuple ? Source extends UnknownArrayOrTuple ? MergeDeepArrayOrTuple<Destination, Source, DefaultMergeDeepOptions<Options>> : never : never >; /** Merge two objects or two arrays/tuples recursively into a new type. - Properties that only exist in one object are copied into the new object. - Properties that exist in both objects are merged if possible or replaced by the one of the source if not. - Top-level arrays and tuples are always spread. - By default, inner arrays and tuples are replaced. See {@link MergeDeepOptions.arrayMergeMode arrayMergeMode} option to change this behaviour. - By default, individual array/tuple elements are not affected. See {@link MergeDeepOptions.recurseIntoArrays recurseIntoArrays} option to change this behaviour. @example ``` import type {MergeDeep} from 'type-fest'; type Foo = { life: number; items: string[]; a: {b: string; c: boolean; d: number[]}; }; interface Bar { name: string; items: number[]; a: {b: number; d: boolean[]}; } type FooBar = MergeDeep<Foo, Bar>; // { // life: number; // name: string; // items: number[]; // a: {b: number; c: boolean; d: boolean[]}; // } type FooBar = MergeDeep<Foo, Bar, {arrayMergeMode: 'spread'}>; // { // life: number; // name: string; // items: (string | number)[]; // a: {b: number; c: boolean; d: (number | boolean)[]}; // } ``` @example ``` import type {MergeDeep} from 'type-fest'; // Merge two arrays type ArrayMerge = MergeDeep<string[], number[]>; // => (string | number)[] // Merge two tuples type TupleMerge = MergeDeep<[1, 2, 3], ['a', 'b']>; // => (1 | 2 | 3 | 'a' | 'b')[] // Merge an array into a tuple type TupleArrayMerge = MergeDeep<[1, 2, 3], string[]>; // => (string | 1 | 2 | 3)[] // Merge a tuple into an array type ArrayTupleMerge = MergeDeep<number[], ['a', 'b']>; // => (number | 'b' | 'a')[] ``` @example ``` import type {MergeDeep, MergeDeepOptions} from 'type-fest'; type Foo = {foo: 'foo'; fooBar: string[]}; type Bar = {bar: 'bar'; fooBar: number[]}; type FooBar = MergeDeep<Foo, Bar>; // { foo: "foo"; bar: "bar"; fooBar: number[]} type FooBarSpread = MergeDeep<Foo, Bar, {arrayMergeMode: 'spread'}>; // { foo: "foo"; bar: "bar"; fooBar: (string | number)[]} type FooBarArray = MergeDeep<Foo[], Bar[]>; // (Foo | Bar)[] type FooBarArrayDeep = MergeDeep<Foo[], Bar[], {recurseIntoArrays: true}>; // FooBar[] type FooBarArraySpreadDeep = MergeDeep<Foo[], Bar[], {recurseIntoArrays: true; arrayMergeMode: 'spread'}>; // FooBarSpread[] type FooBarTupleDeep = MergeDeep<[Foo, true, 42], [Bar, 'life'], {recurseIntoArrays: true}>; // [FooBar, 'life', 42] type FooBarTupleWithArrayDeep = MergeDeep<[Foo[], true], [Bar[], 'life', 42], {recurseIntoArrays: true}>; // [FooBar[], 'life', 42] ``` @example ``` import type {MergeDeep, MergeDeepOptions} from 'type-fest'; function mergeDeep<Destination, Source, Options extends MergeDeepOptions = {}>( destination: Destination, source: Source, options?: Options, ): MergeDeep<Destination, Source, Options> { // Make your implementation ... } ``` @experimental This type is marked as experimental because it depends on {@link ConditionalSimplifyDeep} which itself is experimental. @see {@link MergeDeepOptions} @category Array @category Object @category Utilities */ export type MergeDeep<Destination, Source, Options extends MergeDeepOptions = {}> = MergeDeepWithDefaultOptions< SimplifyDeep<Destination>, SimplifyDeep<Source>, Options >;