diff --git a/type-plus/ts/merge.spec.ts b/type-plus/ts/merge.spec.ts index 4bd9714b84..315de20990 100644 --- a/type-plus/ts/merge.spec.ts +++ b/type-plus/ts/merge.spec.ts @@ -1,59 +1,60 @@ -import { describe, expect, it, test } from '@jest/globals' +import { describe, expect, it } from '@jest/globals' import { testType } from './index.js' import { merge, type Merge } from './merge.js' describe('Merge', () => { - test('same type returns A', () => { - testType.equal, { a: 1 }>(true) + it('merges with any -> any', () => { + testType.equal, any>(true) + testType.equal, any>(true) + testType.equal, any>(true) }) - test('disjoint returns A & B', () => { - testType.equal, { a: 1, b: 1 }>(true) - testType.equal, { a: 1, b?: 1 }>(true) - testType.equal, { a?: 1, b: 1 }>(true) - testType.equal, { a?: 1, b?: 1 }>(true) + it('merges with never -> never', () => { + testType.equal, never>(true) + testType.equal, never>(true) + testType.equal, never>(true) }) - test('replaces property in A with property in B', () => { - testType.equal< - Merge<{ type: 'a' | 'b', value: string }, { value: number }>, - { type: 'a' | 'b', value: number } - >(true) - }) + it('drops unknown', () => { + testType.equal, unknown>(true) - it('removes extra empty {}', () => { - // testType.equal< - // Merge<{ leaf: { boo(): number } }, { leaf: { foo(): number } }>, - // { leaf: { boo(): number } | { foo(): number } } - // >(true) - testType.equal, { leaf: { boo(): number } }>(true) + // intersection type drops `unknown`. `Merge` follows the same pattern. + testType.equal<{ a: 1 } & unknown, { a: 1 }>(true) + testType.equal, { a: 1 }>(true) + testType.equal, { a: 1 }>(true) }) - it('appends types of optional prop to required prop', () => { - testType.equal, { a: number | string }>(true) - }) + it('merges with undefined -> never', () => { + testType.equal, never>(true) - it('appends types of required prop to optional prop', () => { - testType.equal, { a: number }>(true) + // intersection with `undefined` gets `never` so that it will be dropped + testType.equal<{ a: 1 } & undefined, never>(true) + testType.equal, never>(true) + testType.equal, never>(true) }) - it('combines type with required and optional props', () => { - testType.equal, { a: number, b?: string }>(true) + it('merges with void -> never', () => { + testType.equal, never>(true) - type R = Merge< - { a: { c: number } }, - { - a?: { d: string } - } - > + // intersection with `void` SHOULD gets `never` so that it will be dropped. + // but it is returning `{ a: 1 } & void` instead. + // @ts-expect-error + testType.equal<{ a: 1 } & void, never>(true) - testType.inspect(t => t) - // testType.equal(true) + // here we align the behavior with `undefined` + testType.equal, never>(true) + testType.equal, never>(true) }) - it('both optional', () => { - testType.equal, { a?: number | string | undefined }>(true) + it('ignore NonComposableTypes', () => { + testType.equal<{ a: 1 } & string, { a: 1 } & string>(true) + testType.equal<{ a: 1 } & number, { a: 1 } & number>(true) + testType.equal<{ a: 1 } & bigint, { a: 1 } & bigint>(true) + testType.equal<{ a: 1 } & boolean, { a: 1 } & boolean>(true) + testType.equal<{ a: 1 } & symbol, { a: 1 } & symbol>(true) + testType.equal<{ a: 1 } & null, never>(true) + testType.equal<{ a: 1 } & undefined, never>(true) }) }) @@ -149,3 +150,5 @@ describe(`${merge.name}()`, () => { // }) }) }) + +// TODO: array merge check diff --git a/type-plus/ts/merge.ts b/type-plus/ts/merge.ts index fbb202c8a3..aea583ce80 100644 --- a/type-plus/ts/merge.ts +++ b/type-plus/ts/merge.ts @@ -1,10 +1,12 @@ +import type { IsAny } from './any/any_type.js' import type { NonComposableTypes } from './composable_types.js' +import type { IsNever } from './never/never_type.js' import type { AnyRecord } from './object/any_record.js' -import type { IsDisjoint } from './object/IsDisjoint.js' -import type { KeyTypes } from './object/KeyTypes.js' -import type { OptionalKeys } from './object/OptionalKeys.js' -import type { Properties } from './object/properties.js' -import type { IsLiteral } from './predicates/literal.js' +import type { Merge as ObjectMerge } from './object/merge.js' +import type { Or } from './predicates/logical.js' +import type { IsUndefined } from './undefined/undefined_type.js' +import type { IsUnknown } from './unknown/unknown_type.js' +import type { IsVoid } from './void/void_type.js' /** * Left join type `A` with type `B`. @@ -12,51 +14,15 @@ import type { IsLiteral } from './predicates/literal.js' * It handles cases like A or B are `Record`, * joining between required and optional props, etc. */ -export type Merge = [A, B] extends [NonComposableTypes, unknown] ? B - : [A, B] extends [unknown, NonComposableTypes] ? A - : A extends AnyRecord ? B extends AnyRecord - ? IsDisjoint extends true - ? A & B - : ([keyof A, keyof B] extends [infer KA extends KeyTypes, infer KB extends KeyTypes] - ? (IsLiteral extends true - ? (IsLiteral extends true - ? ([OptionalKeys, OptionalKeys] extends [infer PKA extends KeyTypes, infer PKB extends KeyTypes] - ? - // property is optional when both A[k] and B[k] are optional - { - [k in PKA & PKB]?: A[k] | B[k] | undefined - } & - { - [k in Exclude]: Merge.JoinProps - } & - // properties only in A excluding partials is A[k] - { [k in Exclude]: A[k] } & - // properties only in B excluding partials is B[k] - { [k in Exclude]: B[k] } & - // properties is required in A but optional in B is unionized without undefined - { [k in Exclude]: Exclude, undefined> } - : never) - : Properties< - { [k in Exclude]: A[k] } & - { [k in Exclude]: B[k] } & - { [k in KA & KB]: A[k] | B[k] } - >) - : (IsLiteral extends true - ? { [k in Exclude]: A[k] } & { [k in keyof B]: B[k] } - : { [k in Exclude]: A[k] } & { [k in Exclude]: B[k] } & { - [k in KA & KB]: A[k] | B[k] - })) - : never) - : never : never - -export namespace Merge { - export type JoinProps = A extends NonComposableTypes - ? B - : (B extends NonComposableTypes - ? A - : A & B) -} - +export type Merge = Or, IsAny, any, + Or, Or, IsVoid>>, Or, Or, IsVoid>>, never, + IsUnknown : never) + : never)>> + >> /** * Left join `a` with `b`. * @@ -64,7 +30,7 @@ export namespace Merge { * * @example * ```ts -* leftJoin({ a: 1 }, {} as { a?: string | undefined }) // { a: number | string } +* merge({ a: 1 }, {} as { a?: string | undefined }) // { a: number | string } * ``` */ export function merge(a: A, b: B): Merge {