diff --git a/.changeset/lucky-socks-walk.md b/.changeset/lucky-socks-walk.md new file mode 100644 index 0000000000..0ecb626856 --- /dev/null +++ b/.changeset/lucky-socks-walk.md @@ -0,0 +1,5 @@ +--- +"type-plus": major +--- + +Rename `case*` to `$*` to make them easier to use. diff --git a/type-plus/ts/array/array_plus.common_prop_keys.spec.ts b/type-plus/ts/array/array_plus.common_prop_keys.spec.ts index bdb3915807..e39c847bb0 100644 --- a/type-plus/ts/array/array_plus.common_prop_keys.spec.ts +++ b/type-plus/ts/array/array_plus.common_prop_keys.spec.ts @@ -6,7 +6,7 @@ it('never returns never', () => { }) it('can override never case', () => { - testType.equal, 1>(true) + testType.equal, 1>(true) }) it('accepts readonly array', () => { diff --git a/type-plus/ts/array/array_plus.common_prop_keys.ts b/type-plus/ts/array/array_plus.common_prop_keys.ts index be562f4a36..b02a44e67a 100644 --- a/type-plus/ts/array/array_plus.common_prop_keys.ts +++ b/type-plus/ts/array/array_plus.common_prop_keys.ts @@ -15,14 +15,14 @@ import type { KeyTypes } from '../object/KeyTypes.js' * type R = ArrayPlus.CommonPropKeys> // 'a' * ``` * - * @typeParam Options['caseNever'] Return type when `T` is `never`. + * @typeParam Options['$never'] Return type when `T` is `never`. * Default to `never`. */ export type CommonPropKeys< A extends readonly Record[], Options extends CommonPropKeys.Options = CommonPropKeys.DefaultOptions > = NeverType>> ? keyof R : never > diff --git a/type-plus/ts/array/array_plus.element_match.ts b/type-plus/ts/array/array_plus.element_match.ts index 6bb19bf336..381f08a922 100644 --- a/type-plus/ts/array/array_plus.element_match.ts +++ b/type-plus/ts/array/array_plus.element_match.ts @@ -12,13 +12,13 @@ import type { TypePlusOptions } from '../utils/options.js' * e.g. `number, 1` -> `1 | undefined`. * Default to `true`. * - * @typeParam Options['caseNotMatch'] Return value when `T` does not match `Criteria`. + * @typeParam Options['$notMatch'] Return value when `T` does not match `Criteria`. * Default to `never`. * - * @typeParam Options['caseWiden'] Return value when `widen` is true. + * @typeParam Options['$widen'] Return value when `widen` is true. * Default to `Criteria | undefined`. * - * @typeParam Options['caseUnionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`. + * @typeParam Options['$unionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`. * Default to `never`. * * If you want the type to behave more like JavaScript, @@ -38,24 +38,24 @@ export type ElementMatch< ? T : (C['widen'] extends true ? (Criteria extends T - ? C['caseWiden'] - : C['caseNotMatch']) - : C['caseNotMatch'])) extends infer R - ? IsUnion, R> - : C['caseNotMatch']) + ? C['$widen'] + : C['$notMatch']) + : C['$notMatch'])) extends infer R + ? IsUnion, R> + : C['$notMatch']) : never) export namespace ElementMatch { export interface Options { widen?: boolean | undefined, - caseNotMatch?: unknown, - caseWiden?: unknown, - caseUnionNotMatch?: unknown + $notMatch?: unknown, + $widen?: unknown, + $unionNotMatch?: unknown } export interface DefaultOptions { widen: true, - caseNotMatch: never, - caseWiden: Criteria | undefined, - caseUnionNotMatch: never + $notMatch: never, + $widen: Criteria | undefined, + $unionNotMatch: never } } diff --git a/type-plus/ts/array/array_plus.filter.spec.ts b/type-plus/ts/array/array_plus.filter.spec.ts new file mode 100644 index 0000000000..5c67d8f406 --- /dev/null +++ b/type-plus/ts/array/array_plus.filter.spec.ts @@ -0,0 +1,10 @@ +import { it } from '@jest/globals' +import { testType, type ArrayPlus } from '../index.js' + +it('filters empty tuple -> empty tuple', () => { + testType.equal, []>(true) +}) + +it('filters for true elements by default', () => { + testType.equal, [true, true]>(true) +}) diff --git a/type-plus/ts/array/array_plus.filter.ts b/type-plus/ts/array/array_plus.filter.ts index 348310047c..b119d713c4 100644 --- a/type-plus/ts/array/array_plus.filter.ts +++ b/type-plus/ts/array/array_plus.filter.ts @@ -1,3 +1,14 @@ -export type Filter = A[0] extends Criteria - ? A - : Criteria extends A[0] ? Array : never[] +import type { IsEqual } from '../equal/equal.js' + +/** + * Filters an array or tuple based on criteria + */ +export type Filter = Filter._ + +export namespace Filter { + export type _ = A['length'] extends 0 + ? Result + : (A extends [infer H, ...infer Rest] + ? IsEqual, _> + : never) +} diff --git a/type-plus/ts/array/array_plus.find.spec.ts b/type-plus/ts/array/array_plus.find.spec.ts index 4b895a3f61..eb84f94437 100644 --- a/type-plus/ts/array/array_plus.find.spec.ts +++ b/type-plus/ts/array/array_plus.find.spec.ts @@ -12,7 +12,7 @@ it('returns never if input is never', () => { }) it('can override the never case', () => { - testType.equal, 2>(true) + testType.equal, 2>(true) }) it('returns never if the type in the array does not satisfy the criteria', () => { @@ -20,7 +20,7 @@ it('returns never if the type in the array does not satisfy the criteria', () => }) it('can override no_match case', () => { - testType.equal, 'a'>(true) + testType.equal, 'a'>(true) }) it('returns T if T satisfies the Criteria', () => { @@ -35,7 +35,7 @@ it('returns Criteria | undefined if T is a widen type of Criteria', () => { }) it('can override widen case', () => { - testType.equal, never>(true) + testType.equal, never>(true) }) it('does not support tuple', () => { @@ -57,27 +57,27 @@ it('can override unionNotMach to `undefined`', () => { // adding `undefined` to the result better match the behavior in JavaScript, // as an array of `Array` can contains only `string` or `number`. // so `Find, string>` returns `string | undefined`. - testType.equal, number, { caseUnionNotMatch: undefined }>, number | undefined>(true) - testType.equal, number, { caseUnionNotMatch: undefined }>, 1 | 2 | undefined>(true) + testType.equal, number, { $unionNotMatch: undefined }>, number | undefined>(true) + testType.equal, number, { $unionNotMatch: undefined }>, 1 | 2 | undefined>(true) }) it('handles union not match and widen cases separately', () => { testType.equal, 1, { - caseWiden: 234, - caseUnionNotMatch: 123 + $widen: 234, + $unionNotMatch: 123 }>, 123 | 234>(true) }) it('can override the union_miss case', () => { - testType.equal, number, { caseUnionNotMatch: never }>, number>(true) + testType.equal, number, { $unionNotMatch: never }>, number>(true) }) it('will not affect other cases', () => { - testType.equal, number, { caseNever: 123 }>, number | ArrayPlus.Find.DefaultOptions['caseUnionNotMatch']>(true) - testType.equal, ArrayPlus.Find.DefaultOptions['caseNever']>(true) - testType.equal, ArrayPlus.Find.DefaultOptions['caseNotMatch']>(true) - testType.equal, ArrayPlus.Find.DefaultOptions['caseTuple']>(true) - testType.equal, ArrayPlus.Find.DefaultOptions<1>['caseWiden']>(true) + testType.equal, number, { $never: 123 }>, number | ArrayPlus.Find.DefaultOptions['$unionNotMatch']>(true) + testType.equal, ArrayPlus.Find.DefaultOptions['$never']>(true) + testType.equal, ArrayPlus.Find.DefaultOptions['$notMatch']>(true) + testType.equal, ArrayPlus.Find.DefaultOptions['$tuple']>(true) + testType.equal, ArrayPlus.Find.DefaultOptions<1>['$widen']>(true) }) it('support readonly array', () => { diff --git a/type-plus/ts/array/array_plus.find.ts b/type-plus/ts/array/array_plus.find.ts index 756bbbebfa..235a8eb5de 100644 --- a/type-plus/ts/array/array_plus.find.ts +++ b/type-plus/ts/array/array_plus.find.ts @@ -25,20 +25,20 @@ import type { ElementMatch } from './array_plus.element_match.js' * With widen match, a narrowed type will match its widen type. * e.g. matching `1` against `number` yields `1 | undefined` * - * The widen behavior can be customized by `Options['caseWiden']` + * The widen behavior can be customized by `Options['$widen']` * - * @typeParam Options['caseNever'] return type when `A` is `never`. Default to `never`. + * @typeParam Options['$never'] return type when `A` is `never`. Default to `never`. * - * @typeParam Options['caseNotMatch'] Return value when `T` does not match `Criteria`. + * @typeParam Options['$notMatch'] Return value when `T` does not match `Criteria`. * Default to `never`. * - * @typeParam Options['caseTuple'] return type when `A` is a tuple. Default to `not supported` message. + * @typeParam Options['$tuple'] return type when `A` is a tuple. Default to `not supported` message. * - * @typeParam Options['caseWiden'] return type when `T` in `A` is a widen type of `Criteria`. + * @typeParam Options['$widen'] return type when `T` in `A` is a widen type of `Criteria`. * Default to `Criteria | undefined`. * Set it to `never` for a more type-centric behavior * - * @typeParam Options['caseUnionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`. + * @typeParam Options['$unionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`. * Default to `never`. * * If you want the type to behave more like JavaScript, @@ -54,7 +54,7 @@ export type Find< TypePlusOptions.Merge> extends infer O extends Find.Options ? TupleType< A, - O['caseTuple'], + O['$tuple'], A extends Readonly> ? ElementMatch : never, O > @@ -62,10 +62,10 @@ export type Find< export namespace Find { export interface Options extends ElementMatch.Options, NeverType.Options { - caseTuple?: unknown, + $tuple?: unknown, } export interface DefaultOptions extends ElementMatch.DefaultOptions, NeverType.DefaultOptions { - caseTuple: 'does not support tuple. Please use `FindFirst` or `TuplePlus.Find` instead.', + $tuple: 'does not support tuple. Please use `FindFirst` or `TuplePlus.Find` instead.', } } diff --git a/type-plus/ts/array/array_plus.is_readonly.spec.ts b/type-plus/ts/array/array_plus.is_readonly.spec.ts index 027b7c4034..cba56b40bb 100644 --- a/type-plus/ts/array/array_plus.is_readonly.spec.ts +++ b/type-plus/ts/array/array_plus.is_readonly.spec.ts @@ -24,13 +24,13 @@ it('detects regular tuple as not readonly', () => { it('', () => { testType.equal, false>(true) testType.equal, false>(true) - testType.equal, 'n'>(true) + testType.equal, 'n'>(true) testType.equal, false>(true) }) it('detects non array case', () => { - testType.equal, 'n'>(true) + testType.equal, 'n'>(true) }) it('distributes over union', () => { diff --git a/type-plus/ts/array/array_plus.is_readonly.ts b/type-plus/ts/array/array_plus.is_readonly.ts index 2c2f97f4a5..432e97c566 100644 --- a/type-plus/ts/array/array_plus.is_readonly.ts +++ b/type-plus/ts/array/array_plus.is_readonly.ts @@ -24,24 +24,24 @@ export type IsReadonly< TypePlusOptions.Merge extends infer O extends IsReadonly.Options ? NeverType< A, - O['caseNever'], + O['$never'], A extends any ? LooseArrayType extends A ? O['caseThen'] : O['caseElse'], - O['caseNotArray'] + Readonly extends A ? O['$then'] : O['$else'], + O['$notArray'] > : never > : never export namespace IsReadonly { export interface Options extends NeverType.Options, TypePlusOptions.Selection { - caseNotArray?: unknown + $notArray?: unknown } export interface DefaultOptions { - caseThen: true, - caseElse: false, - caseNever: false, - caseNotArray: false + $then: true, + $else: false, + $never: false, + $notArray: false } } diff --git a/type-plus/ts/array/find_first.spec.ts b/type-plus/ts/array/find_first.spec.ts index 686567b3ec..e7807e3b01 100644 --- a/type-plus/ts/array/find_first.spec.ts +++ b/type-plus/ts/array/find_first.spec.ts @@ -17,7 +17,7 @@ describe('For Array', () => { }) it('can override widen case', () => { - testType.equal, never>(true) + testType.equal, never>(true) }) it('returns Criteria if T is a union partially satisfies the Criteria', () => { @@ -29,8 +29,8 @@ describe('For Array', () => { // adding `undefined` to the result better match the behavior in JavaScript, // as an array of `Array` can contains only `string` or `number`. // so `Find, string>` returns `string | undefined`. - testType.equal, number, { caseUnionNotMatch: undefined }>, number | undefined>(true) - testType.equal, number, { caseUnionNotMatch: undefined }>, 1 | 2 | undefined>(true) + testType.equal, number, { $unionNotMatch: undefined }>, number | undefined>(true) + testType.equal, number, { $unionNotMatch: undefined }>, 1 | 2 | undefined>(true) }) it('support readonly array', () => { testType.equal>, number>, number>(true) diff --git a/type-plus/ts/array/find_first.ts b/type-plus/ts/array/find_first.ts index b941c03541..9cb51f85af 100644 --- a/type-plus/ts/array/find_first.ts +++ b/type-plus/ts/array/find_first.ts @@ -31,21 +31,21 @@ import type { Find as ArrayFind } from './array_plus.find.js' * With widen match, a narrowed type will match its widen type. * e.g. matching `1` against `number` yields `1 | undefined` * - * The widen behavior can be customized by `Options['caseWiden']` + * The widen behavior can be customized by `Options['$widen']` * * @typeParam Options['caseEmptyTuple'] return type when `A` is an empty tuple. * Default to `never`. * - * @typeParam Options['caseNever'] return type when `A` is `never`. Default to `never`. + * @typeParam Options['$never'] return type when `A` is `never`. Default to `never`. * - * @typeParam Options['caseNoMatch'] Return value when `T` does not match `Criteria`. + * @typeParam Options['$noMatch'] Return value when `T` does not match `Criteria`. * Default to `never`. * - * @typeParam Options['caseWiden'] return type when `T` in `A` is a widen type of `Criteria`. + * @typeParam Options['$widen'] return type when `T` in `A` is a widen type of `Criteria`. * Default to `Criteria | undefined`. * Set it to `never` for a more type-centric behavior * - * @typeParam Options['caseUnionMiss'] Return value when a branch of the union `T` does not match `Criteria`. + * @typeParam Options['$unionMiss'] Return value when a branch of the union `T` does not match `Criteria`. * Default to `undefined`. * Since it is a union, the result will be join to the matched branch as union. */ diff --git a/type-plus/ts/array/head.spec.ts b/type-plus/ts/array/head.spec.ts index ccebd04086..16f6c2d9f2 100644 --- a/type-plus/ts/array/head.spec.ts +++ b/type-plus/ts/array/head.spec.ts @@ -6,7 +6,7 @@ it('returns never for empty tuple', () => { }) it('can override never case', () => { - testType.equal, 1>(true) + testType.equal, 1>(true) }) it('gets the type of an array', () => { diff --git a/type-plus/ts/array/head.ts b/type-plus/ts/array/head.ts index 8cf2c67a27..154eb1cda9 100644 --- a/type-plus/ts/array/head.ts +++ b/type-plus/ts/array/head.ts @@ -14,7 +14,7 @@ import type { NeverType } from '../never/never_type.js' * type R = Head<[]> // never * ``` * - * @typeParam Options['caseNever'] Return type when `T` is `never`. + * @typeParam Options['$never'] Return type when `T` is `never`. * Default to `never`. * * @typeParam Options['caseEmptyTuple'] Return type when `T` is `[]`. @@ -25,7 +25,7 @@ export type Head< Options extends Head.Options = Head.DefaultOptions > = NeverType< T, - Options['caseNever'], + Options['$never'], T['length'] extends 0 ? Options['caseEmptyTuple'] : T[0] > diff --git a/type-plus/ts/array/last.spec.ts b/type-plus/ts/array/last.spec.ts index cfb547015f..20cb05cd51 100644 --- a/type-plus/ts/array/last.spec.ts +++ b/type-plus/ts/array/last.spec.ts @@ -6,7 +6,7 @@ it('returns never for empty tuple', () => { }) it('can override never case', () => { - testType.equal, 1>(true) + testType.equal, 1>(true) }) it('gets the type of an array', () => { diff --git a/type-plus/ts/array/last.ts b/type-plus/ts/array/last.ts index 84561506bf..a2e0d6236c 100644 --- a/type-plus/ts/array/last.ts +++ b/type-plus/ts/array/last.ts @@ -13,7 +13,7 @@ import type { NeverType } from '../never/never_type.js' * type R = Last<[]> // never * ``` * - * @typeParam Options['caseNever'] Return type when `T` is `never`. + * @typeParam Options['$never'] Return type when `T` is `never`. * Default to `never`. * * @typeParam Options['caseEmptyTuple'] Return type when `T` is `[]`. @@ -23,7 +23,7 @@ export type Last< T extends readonly unknown[], Options extends Last.Options = Last.DefaultOptions > = NeverType` +`FindFirst` 🦴 *utilities* 🔢 *customizable* @@ -154,11 +154,11 @@ type R = FindFirst // never // customization type R = FindFirst<[number], 1, { widen: false }> // never -type R = FindFirst<[number], 1, { caseWiden: never }> // never +type R = FindFirst<[number], 1, { $widen: never }> // never type R = FindFirst<[], 1, { caseEmptyTuple: 2 }> // 2 -type R = FindFirst // 2 -type R = FindFirst<[string], number, { caseNotMatch: 2 }> // 2 -type R = FindFirst<[string | number], number, { caseUnionNotMatch: undefined }> // number | undefined +type R = FindFirst // 2 +type R = FindFirst<[string], number, { $notMatch: 2 }> // 2 +type R = FindFirst<[string | number], number, { $unionNotMatch: undefined }> // number | undefined ``` ## [`FindLast`](./array.find_last.tsl19) @@ -198,7 +198,7 @@ type R = KeepMatch, string> // string[] ## [`Head`](./head.ts#l23) -`Head` +`Head` 🦴 *utilities* 🔢 *customizable* @@ -210,11 +210,11 @@ import type { Head } from 'type-plus' type R = Head<[1, 2, 3]> // 1 type R = Head // string -type R = Head // caseNever: never +type R = Head // $never: never type R = Head<[]> // caseEmptyTuple: never // customization -type R = Head // 1 +type R = Head // 1 type R = Head<[], { caseEmptyTuple: undefined }> // undefined ``` @@ -224,7 +224,7 @@ type R = Head<[], { caseEmptyTuple: undefined }> // undefined ## [`Last`](./last.ts#l23) -`Last` +`Last` 🦴 *utilities* 🔢 *customizable* @@ -236,11 +236,11 @@ import type { Last } from 'type-plus' type R = Last<[1, 2, 3]> // 3 type R = Last // string -type R = Last // caseNever: never +type R = Last // $never: never type R = Last<[]> // caseEmptyTuple: never // customization -type R = Last // 1 +type R = Last // 1 type R = Last<[], { caseEmptyTuple: undefined }> // undefined ``` @@ -275,7 +275,7 @@ Alias of [At](#at). ## [ArrayPlus.CommonPropKeys](./array_plus.common_prop_keys.ts#l21) -`ArrayPlus.CommonPropKeys` +`ArrayPlus.CommonPropKeys` ⚗️ *transform* 🔢 *customizable* @@ -289,7 +289,7 @@ type R = ArrayPlus.CommonPropKeys> // 'a' type R = ArrayPlus.CommonPropKeys> // 'a' // customization -type R = ArrayPlus.CommonPropKeys // 1 +type R = ArrayPlus.CommonPropKeys // 1 ``` ### [`ArrayPlus.Concat`](./array.concat.ts#l12) @@ -300,7 +300,7 @@ Alias of [Concat](#concat). ### [`ArrayPlus.ElementMatch`](./array_plus.element_match.ts#l30) -`ArrayPlus.ElementMatch` +`ArrayPlus.ElementMatch` 🌪️ *filter* 🔢 *customizable* @@ -317,10 +317,10 @@ type R = ArrayPlus.ElementMatch // widen: 1 type R = ArrayPlus.ElementMatch // unionNotMatch: number // customization -type R = ArrayPlus.ElementMatch // 1 +type R = ArrayPlus.ElementMatch // 1 type R = ArrayPlus.ElementMatch // never -type R = ArrayPlus.ElementMatch // never -type R = ArrayPlus.ElementMatch // number | undefined +type R = ArrayPlus.ElementMatch // never +type R = ArrayPlus.ElementMatch // number | undefined ``` ### [`ArrayPlus.Entries`](./array.entries.ts#L14) @@ -339,7 +339,7 @@ type R = ArrayPlus.Entries<[1, 2, 3]> // [[0, 1], [1, 2], [2, 3]] ### [`ArrayPlus.Find`](./array_plus.find.ts#l49) -`ArrayPlus.Find` +`ArrayPlus.Find` 🦴 *utilities* 🔢 *customizable* @@ -359,11 +359,11 @@ type R = ArrayPlus.Find // never // customization type R = ArrayPlus.Find // never -type R = ArrayPlus.Find // never -type R = ArrayPlus.Find // 2 -type R = ArrayPlus.Find // 2 -type R = ArrayPlus.Find<[], 1, { caseTuple: 2 }> // 2 -type R = ArrayPlus.Find, number, { caseUnionNotMatch: undefined }> // number | undefined +type R = ArrayPlus.Find // never +type R = ArrayPlus.Find // 2 +type R = ArrayPlus.Find // 2 +type R = ArrayPlus.Find<[], 1, { $tuple: 2 }> // 2 +type R = ArrayPlus.Find, number, { $unionNotMatch: undefined }> // number | undefined ``` ### [`ArrayPlus.FindLast`](./array.find_last.ts#L17) @@ -414,7 +414,7 @@ type R = IsIndexOutOfBound<[1], -2> // true ### [`ArrayPlus.IsReadonly](./array_plus.is_readonly.ts#l19) -`ArrayPlus.IsReadonly` +`ArrayPlus.IsReadonly` 🎭 *validate* 🔢 *customizable* @@ -429,10 +429,10 @@ type R = IsReadonly<[1, 2, 3, 4, 5]> // false type R = IsReadonly // boolean // customization -type R = IsReadonly // 1 -type R = IsReadonly // 1 -type R = IsReadonly // 1 -type R = IsReadonly // 1 +type R = IsReadonly // 1 +type R = IsReadonly // 1 +type R = IsReadonly // 1 +type R = IsReadonly // 1 ``` ### [`ArrayPlus.Reverse`](./array.reverse.ts#l14) diff --git a/type-plus/ts/index.ts b/type-plus/ts/index.ts index bf00223b1f..2837d069cd 100644 --- a/type-plus/ts/index.ts +++ b/type-plus/ts/index.ts @@ -82,6 +82,7 @@ export type { export type { IsNotPositive, IsPositive, NonPositive, Positive } from './numeric/positive.js' export type { Required, RequiredExcept, RequiredPick } from './object/Required.js' export * from './object/index.js' +export * as ObjectPlus from './object/object_plus.js' export type { IsNotObject, IsObject, NotObjectType, ObjectType } from './object/object_type.js' export * from './predicates/index.js' export type { PrimitiveTypes } from './primitive.js' diff --git a/type-plus/ts/never/never_type.ts b/type-plus/ts/never/never_type.ts index aa2dec6a9f..1e782049cf 100644 --- a/type-plus/ts/never/never_type.ts +++ b/type-plus/ts/never/never_type.ts @@ -61,10 +61,10 @@ export namespace NeverType { * Type options when input type is `never`. */ export interface Options { - caseNever?: unknown + $never?: unknown } export interface DefaultOptions { - caseNever: never + $never: never } } diff --git a/type-plus/ts/object/merge.spec.ts b/type-plus/ts/object/merge.spec.ts new file mode 100644 index 0000000000..0b3a2879e2 --- /dev/null +++ b/type-plus/ts/object/merge.spec.ts @@ -0,0 +1,119 @@ +import { expect, it } from '@jest/globals' +import { ObjectPlus, testType } from '../index.js' + +it('merges with any -> any', () => { + testType.equal, any>(true) + testType.equal, any>(true) + testType.equal, any>(true) +}) + +it('merges with never -> never', () => { + testType.equal, never>(true) + testType.equal, never>(true) + testType.equal, never>(true) +}) + +it('returns A if A and B are the same type', () => { + testType.equal, { a: 1 }>(true) +}) + +it('merges disjoint types', () => { + testType.equal, { a: 1, b: 1 }>(true) +}) + +it('combines type with required and optional props', () => { + testType.equal, { a: 1, b?: 1 }>(true) + testType.equal, { a: 1, b?: 1 | undefined }>(true) + + testType.equal, { a?: 1, b: 1 }>(true) + testType.equal, { a?: 1 | undefined, b: 1 }>(true) + + testType.equal, { a?: 1, b?: 1 }>(true) + testType.equal, { a?: 1 | undefined, b?: 1 }>(true) + testType.equal, { a?: 1, b?: 1 | undefined }>(true) + testType.equal, { a?: 1 | undefined, b?: 1 | undefined }>(true) +}) + +it('replaces property in A with property in B', () => { + testType.equal< + ObjectPlus.Merge<{ type: 'a' | 'b', value: string }, { value: number }>, + { type: 'a' | 'b', value: number } + >(true) +}) + +it('removes extra empty {}', () => { + testType.equal, { leaf: { boo(): number } }>(true) +}) + +it('overrides property in A with property in B', () => { + const a: { leaf: { foo: 'foo' } } = { leaf: { foo: 'foo' } } + const b: { leaf: { boo: 'boo' } } = { leaf: { boo: 'boo' } } + + expect({ ...a, ...b }).toEqual({ leaf: { boo: 'boo' } }) + + testType.equal< + ObjectPlus.Merge<{ leaf: { foo: 'foo' } }, { leaf: { boo: 'boo' } }>, + { leaf: { boo: 'boo' } } + >(true) +}) + +it('appends types of optional prop to required prop', () => { + const x: { a: number } = { a: 1 } + const y: { a?: string } = {} + + expect({ ...x, ...y }).toEqual({ a: 1 }) + + testType.equal< + ObjectPlus.Merge<{ a: number }, { a?: string }>, + { a: number | string } + >(true) + + testType.equal< + ObjectPlus.Merge<{ a: number }, { a?: string | undefined }>, + { a: number | string } + >(true) +}) + +it('appends types of required prop to optional prop', () => { + testType.equal, { a: number }>(true) +}) + +it('combines type with required and optional props', () => { + testType.equal, { a: number, b?: string }>(true) + + type R = ObjectPlus.Merge< + { a: { c: number } }, + { + a?: { d: string } + } + > + + testType.inspect(t => t) + testType.equal(true) +}) + +it('merges an optional property with a required property merges the two as union', () => { + type X = { a: { c: number } } + type Y = { a?: { d: string } } + const x: X = { a: { c: 1 } } + const y1: Y = {} + const y2: Y = { a: { d: 'd' } } + expect({ ...x, ...y1 }).toEqual({ a: { c: 1 } }) + testType.canAssign<{ a: { c: number } }, ObjectPlus.Merge>(true) + + expect({ ...x, ...y2 }).toEqual({ a: { d: 'd' } }) + testType.canAssign<{ a: { d: string } }, ObjectPlus.Merge>(true) + + testType.equal, { a: { c: number } | { d: string } }>(true) +}) + +it('merges two optional properties', () => { + testType.equal, { a?: number | string }>(true) + testType.equal, { a?: number | string | undefined }>(true) + testType.equal, { a?: number | string | undefined }>(true) + testType.equal, + { a?: number | string | undefined } + >(true) +}) + diff --git a/type-plus/ts/object/merge.ts b/type-plus/ts/object/merge.ts new file mode 100644 index 0000000000..a5c35957b9 --- /dev/null +++ b/type-plus/ts/object/merge.ts @@ -0,0 +1,116 @@ +import type { IsAny } from '../any/any_type.js' +import type { NonComposableTypes } from '../composable_types.js' +import type { IsNever, NotNeverType } from '../never/never_type.js' +import type { IsLiteral } from '../predicates/literal.js' +import type { Or } from '../predicates/logical.js' +import type { $Never } from '../type_plus/cases.js' +import type { AnyRecord } from './any_record.js' +import type { IsDisjoint } from './IsDisjoint.js' +import type { KeyTypes } from './KeyTypes.js' +import type { OptionalKeys } from './OptionalKeys.js' + +export type Merge = Or< + IsAny, + IsAny, + any, + Or< + IsNever, + IsNever, + never, + 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 + NotNeverType< + PKA & PKB, + { [k in PKA & PKB]?: A[k] | B[k] }, + unknown + > & + // (NotNeverType< + // Exclude, + // { [k in Exclude]: Merge.JoinProps }, + // unknown + // >) & + // properties only in A excluding partials is A[k] + NotNeverType< + Exclude, + { [k in Exclude]: A[k] }, + unknown + > & + // properties only in B excluding partials is B[k] + NotNeverType< + Exclude, + { [k in Exclude]: B[k] }, + unknown + > & + // properties is required in A but optional in B is unionized without undefined + NotNeverType< + Exclude, + { [k in Exclude]: A[k] | Exclude }, + unknown + > + : never) + : + NotNeverType< + Exclude, + { [k in Exclude]: A[k] }, + unknown + > & + NotNeverType< + Exclude, + { [k in Exclude]: B[k] }, + unknown + > & + NotNeverType< + KA & KB, + { [k in KA & KB]: A[k] | B[k] }, + unknown + > + ) + : ( + IsLiteral extends true + ? { [k in Exclude]: A[k] } & { [k in keyof B]: B[k] } + : + NotNeverType< + Exclude, + { [k in Exclude]: A[k] }, + unknown + > & + NotNeverType< + Exclude, + { [k in Exclude]: B[k] }, + unknown + > & + NotNeverType< + KA & KB, + { [k in KA & KB]: A[k] | B[k] }, + unknown + > + )) + : never) + > +> + +export namespace Merge { + export type JoinProps = A extends NonComposableTypes + ? B + : (B extends NonComposableTypes + ? A + : A & B) + + export type Options = { + $never?: undefined + } + + export interface DefaultOptions { + $never: never + } + + export type Cases = { + $never: $Never + } +} diff --git a/type-plus/ts/object/object_plus.ts b/type-plus/ts/object/object_plus.ts new file mode 100644 index 0000000000..9974dbde12 --- /dev/null +++ b/type-plus/ts/object/object_plus.ts @@ -0,0 +1 @@ +export * from './merge.js' diff --git a/type-plus/ts/tuple/common_prop_keys.spec.ts b/type-plus/ts/tuple/common_prop_keys.spec.ts index 75200d5e1d..271931a59f 100644 --- a/type-plus/ts/tuple/common_prop_keys.spec.ts +++ b/type-plus/ts/tuple/common_prop_keys.spec.ts @@ -6,7 +6,7 @@ it('never returns never', () => { }) it('can override never case', () => { - testType.equal, 1>(true) + testType.equal, 1>(true) }) it('returns all common keys from record entry in array', () => { diff --git a/type-plus/ts/tuple/common_prop_keys.ts b/type-plus/ts/tuple/common_prop_keys.ts index 03097da1c5..c6564716a5 100644 --- a/type-plus/ts/tuple/common_prop_keys.ts +++ b/type-plus/ts/tuple/common_prop_keys.ts @@ -16,7 +16,7 @@ import type { CommonPropKeys as TupleCommonPropKeys } from './tuple_plus.common_ * type R = CommonPropKeys<[{ a: number, c: 1 }, { b: number, c: 2 }]> // 'c' * ``` * - * @typeParam Options['caseNever'] Return type when `T` is `never`. + * @typeParam Options['$never'] Return type when `T` is `never`. * Default to `never`. */ export type CommonPropKeys< diff --git a/type-plus/ts/tuple/drop.ts b/type-plus/ts/tuple/drop.ts index 19b9d3a73d..7989c2e619 100644 --- a/type-plus/ts/tuple/drop.ts +++ b/type-plus/ts/tuple/drop.ts @@ -17,7 +17,7 @@ import type { DropMatch as TupleDropMatch } from './tuple_plus.drop_match.js' * type R = DropFirst // string[] * ``` * - * @typeParam Options['caseArray'] Return type when `T` is `Array`. + * @typeParam Options['$array'] Return type when `T` is `Array`. * Default to `T`. * * @typeParam Options['caseEmptyTuple'] Return type when `T` is an empty tuple. @@ -27,7 +27,7 @@ export type DropFirst< T extends unknown[], Options extends DropFirst.Options = DropFirst.DefaultOptions > = number extends T['length'] - ? Options['caseArray'] + ? Options['$array'] : T['length'] extends 0 ? Options['caseEmptyTuple'] : T['length'] extends 1 @@ -38,11 +38,11 @@ export type DropFirst< export namespace DropFirst { export interface Options { - caseArray?: unknown, + $array?: unknown, caseEmptyTuple?: unknown, } export interface DefaultOptions { - caseArray: T, + $array: T, caseEmptyTuple: [] } } @@ -63,7 +63,7 @@ export namespace DropFirst { * type R = DropLast // string[] * ``` * - * @typeParam Options['caseArray'] Return type when `T` is `Array`. + * @typeParam Options['$array'] Return type when `T` is `Array`. * Default to `T`. * * @typeParam Options['caseEmptyTuple'] Return type when `T` is an empty tuple. @@ -73,7 +73,7 @@ export type DropLast< T extends unknown[], Cases extends DropLast.Options = DropLast.DefaultOptions > = number extends T['length'] - ? Cases['caseArray'] + ? Cases['$array'] : T['length'] extends 0 ? Cases['caseEmptyTuple'] : T['length'] extends 1 @@ -85,11 +85,11 @@ export type DropLast< export namespace DropLast { export interface Options { - caseArray?: unknown, + $array?: unknown, caseEmptyTuple?: unknown, } export interface DefaultOptions { - caseArray: T, + $array: T, caseEmptyTuple: [] } } diff --git a/type-plus/ts/tuple/readme.md b/type-plus/ts/tuple/readme.md index 149f59a393..ee0d770e7b 100644 --- a/type-plus/ts/tuple/readme.md +++ b/type-plus/ts/tuple/readme.md @@ -106,7 +106,7 @@ Overridable cases: ## [CommonPropKeys](./common_prop_keys.ts#l22) -`CommonPropKeys` +`CommonPropKeys` ⚗️ *transform* 🔢 *customizable* @@ -120,15 +120,15 @@ type R = CommonPropKeys<[{ a: 1, c: 1 }, { b: 1, c: 2 }]> // 'c' type R = CommonPropKeys<[{ a: 1 }, { b: 1 }]> // never type R = CommonPropKeys> // 'a' type R = CommonPropKeys<[{ a: 1 }, { b: 1 }]> // never -type R = CommonPropKeys // caseNever: never +type R = CommonPropKeys // $never: never // customization -type R = CommonPropKeys // 1 +type R = CommonPropKeys // 1 ``` ## [DropFirst](./drop.ts#l26) -`DropFirst` +`DropFirst` ⚗️ *transform* 🔢 *customizable* @@ -140,11 +140,11 @@ import { DropFirst } from 'type-plus' type R = DropFirst<[1, 2, 3]> // [2, 3] type R = DropFirst<[string]> // [] -type R = DropFirst // caseArray: string[] +type R = DropFirst // $array: string[] type R = DropFirst<[]> // caseEmptyTuple: [] // customization -type R = DropFirst // 1 +type R = DropFirst // 1 type R = DropFirst<[], { caseEmptyTuple: 1 }> // 1 ``` @@ -162,11 +162,11 @@ import { DropLast } from 'type-plus' type R = DropLast<[1, 2, 3]> // [2, 3] type R = DropLast<[string]> // [] -type R = DropLast // caseArray: string[] +type R = DropLast // $array: string[] type R = DropLast<[]> // caseEmptyTuple: [] // customization -type R = DropLast // 1 +type R = DropLast // 1 type R = DropLast<[], { caseEmptyTuple: 1 }> // 1 ``` @@ -191,7 +191,7 @@ The input type are not checked and assumed to be *tuple*. ## [TuplePlus.CommonPropKeys](./tuple_plus.common_prop_keys.ts#l22) -`TuplePlus.CommonPropKeys` +`TuplePlus.CommonPropKeys` ⚗️ *transform* 🔢 *customizable* @@ -205,10 +205,10 @@ type R = TuplePlus.CommonPropKeys<[{ a: 1, c: 1 }, { b: 1, c: 2 }]> // 'c' type R = TuplePlus.CommonPropKeys<[{ a: 1 }, { b: 1 }]> // never type R = TuplePlus.CommonPropKeys> // 'a' type R = TuplePlus.CommonPropKeys<[{ a: 1 }, { b: 1 }]> // never -type R = TuplePlus.CommonPropKeys // caseNever: never +type R = TuplePlus.CommonPropKeys // $never: never // customization -type R = TuplePlus.CommonPropKeys // 1 +type R = TuplePlus.CommonPropKeys // 1 ``` ### [TuplePlus.Filter](./tuple_plus.filter.ts) @@ -227,7 +227,7 @@ type R = TuplePlus.Filter<[1, 2, '3'], number> // [1, 2] ### [`TuplePlus.Find`](./tuple_plus.find.ts#l51) -`TuplePlus.Find` +`TuplePlus.Find` 🦴 *utilities* 🔢 *customizable* @@ -246,12 +246,12 @@ type R = TuplePlus.Find<[true, 1, 'x'], 2> // never // customization type R = TuplePlus.Find<[number], 1, { widen: false }> // never -type R = TuplePlus.Find<[number], 1, { caseWiden: never }> // never -type R = TuplePlus.Find // 2 +type R = TuplePlus.Find<[number], 1, { $widen: never }> // never +type R = TuplePlus.Find // 2 type R = TuplePlus.Find<[], 1, { caseEmptyTuple: 2 }> // 2 -type R = TuplePlus.Find // 2 -type R = TuplePlus.Find<[string], number, { caseNotMatch: 2 }> // 2 -type R = TuplePlus.Find<[string | number], number, { caseUnionNotMatch: undefined }> // number | undefined +type R = TuplePlus.Find // 2 +type R = TuplePlus.Find<[string], number, { $notMatch: 2 }> // 2 +type R = TuplePlus.Find<[string | number], number, { $unionNotMatch: undefined }> // number | undefined ``` ### [TuplePlus.PadStart](./tuple_plus.pad_start.ts) diff --git a/type-plus/ts/tuple/tuple_plus.common_prop_keys.spec.ts b/type-plus/ts/tuple/tuple_plus.common_prop_keys.spec.ts index 8aaa9c032f..3f712bd145 100644 --- a/type-plus/ts/tuple/tuple_plus.common_prop_keys.spec.ts +++ b/type-plus/ts/tuple/tuple_plus.common_prop_keys.spec.ts @@ -6,7 +6,7 @@ it('never returns never', () => { }) it('can override never case', () => { - testType.equal, 1>(true) + testType.equal, 1>(true) }) it('returns never for empty tuple', () => { diff --git a/type-plus/ts/tuple/tuple_plus.common_prop_keys.ts b/type-plus/ts/tuple/tuple_plus.common_prop_keys.ts index e7486ba108..be5cd8b1a0 100644 --- a/type-plus/ts/tuple/tuple_plus.common_prop_keys.ts +++ b/type-plus/ts/tuple/tuple_plus.common_prop_keys.ts @@ -16,7 +16,7 @@ import type { Tail } from './tail.js' * type R = TuplePlus.CommonPropKeys<[{ a: number, c: 1 }, { b: number, c: 2 }]> // 'c' * ``` * - * @typeParam Options['caseNever'] Return type when `T` is `never`. + * @typeParam Options['$never'] Return type when `T` is `never`. * Default to `never`. */ export type CommonPropKeys< @@ -24,7 +24,7 @@ export type CommonPropKeys< Options extends CommonPropKeys.Options = CommonPropKeys.DefaultOptions > = NeverType< T, - Options['caseNever'], + Options['$never'], (T['length'] extends 0 ? never : ( diff --git a/type-plus/ts/tuple/tuple_plus.find.spec.ts b/type-plus/ts/tuple/tuple_plus.find.spec.ts index 61e0620c7c..27982084db 100644 --- a/type-plus/ts/tuple/tuple_plus.find.spec.ts +++ b/type-plus/ts/tuple/tuple_plus.find.spec.ts @@ -12,7 +12,7 @@ it('returns never if input is never', () => { }) it('can override the never case', () => { - testType.equal, 2>(true) + testType.equal, 2>(true) }) it('returns never for empty tuple', () => { @@ -20,7 +20,7 @@ it('returns never for empty tuple', () => { }) it('can override empty tuple case', () => { - testType.equal, 1>(true) + testType.equal, 1>(true) }) it('does not work with array type', () => { @@ -28,7 +28,7 @@ it('does not work with array type', () => { }) it('can override array case', () => { - testType.equal, 1>(true) + testType.equal, 1>(true) }) it('no match gets never', () => { @@ -36,7 +36,7 @@ it('no match gets never', () => { }) it('can override no_match case', () => { - testType.equal, 1>(true) + testType.equal, 1>(true) }) it('pick first type matching criteria', () => { @@ -59,7 +59,7 @@ it('returns Criteria | undefined if T is a widen type of Criteria', () => { }) it('can override widen case', () => { - testType.equal, 12>(true) + testType.equal, 12>(true) }) it('can disable widen', () => { @@ -74,7 +74,7 @@ it('can return T | undefined by overriding unionNotMach to `undefined`', () => { // adding `undefined` to the result better match the behavior in JavaScript, // as an array of `Array` can contains only `string` or `number`. // so `Find, string>` returns `string | undefined`. - testType.equal, number | undefined>(true) + testType.equal, number | undefined>(true) }) it('pick object', () => { diff --git a/type-plus/ts/tuple/tuple_plus.find.ts b/type-plus/ts/tuple/tuple_plus.find.ts index e6eefaad16..36bb2c88c1 100644 --- a/type-plus/ts/tuple/tuple_plus.find.ts +++ b/type-plus/ts/tuple/tuple_plus.find.ts @@ -24,23 +24,23 @@ import type { TupleType } from './tuple_type.js' * With widen match, a narrowed type will match its widen type. * e.g. matching `1` against `number` yields `1 | undefined` * - * The widen behavior can be customized by `Options['caseWiden']` + * The widen behavior can be customized by `Options['$widen']` * - * @typeParam Options['caseArray'] return type when `A` is an array. Default to `not supported` message. + * @typeParam Options['$array'] return type when `A` is an array. Default to `not supported` message. * * @typeParam Options['caseEmptyTuple'] return type when `A` is an empty tuple. * Default to `never`. * - * @typeParam Options['caseNever'] return type when `A` is `never`. Default to `never`. + * @typeParam Options['$never'] return type when `A` is `never`. Default to `never`. * - * @typeParam Options['caseNotMatch'] Return value when `T` does not match `Criteria`. + * @typeParam Options['$notMatch'] Return value when `T` does not match `Criteria`. * Default to `never`. * - * @typeParam Options['caseWiden'] return type when `T` in `A` is a widen type of `Criteria`. + * @typeParam Options['$widen'] return type when `T` in `A` is a widen type of `Criteria`. * Default to `Criteria | undefined`. * Set it to `never` for a more type-centric behavior * - * @typeParam Options['caseUnionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`. + * @typeParam Options['$unionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`. * Default to `never`. * * If you want the type to behave more like JavaScript, @@ -56,9 +56,9 @@ export type Find< ? TupleType< A, A['length'] extends 0 - ? O['caseEmptyTuple'] + ? O['$emptyTuple'] : Find.Device, - O['caseArray'], + O['$array'], O > : never @@ -68,21 +68,21 @@ export namespace Find { Criteria, Options extends Find.Options > = A['length'] extends 0 - ? Options['caseNotMatch'] + ? Options['$notMatch'] : (A extends readonly [infer Head, ...infer Tail] ? ElementMatch< Head, Criteria, - TypePlusOptions.Merge<{ caseNotMatch: Device }, Options> + TypePlusOptions.Merge<{ $notMatch: Device }, Options> > : never) export interface Options extends ElementMatch.Options, NeverType.Options { - caseArray?: unknown, - caseEmptyTuple?: unknown, + $array?: unknown, + $emptyTuple?: unknown, } export interface DefaultOptions extends ElementMatch.DefaultOptions, NeverType.DefaultOptions { - caseArray: 'does not support array. Please use `FindFirst` or `ArrayPlus.Find` instead.', - caseEmptyTuple: never, + $array: 'does not support array. Please use `FindFirst` or `ArrayPlus.Find` instead.', + $emptyTuple: never, } } diff --git a/type-plus/ts/tuple/tuple_type.is_not_tuple.spec.ts b/type-plus/ts/tuple/tuple_type.is_not_tuple.spec.ts index 9a2d133abb..1c066e0902 100644 --- a/type-plus/ts/tuple/tuple_type.is_not_tuple.spec.ts +++ b/type-plus/ts/tuple/tuple_type.is_not_tuple.spec.ts @@ -59,5 +59,5 @@ it('can override Then/Else', () => { }) it('can override never case', () => { - testType.equal, 3>(true) + testType.equal, 3>(true) }) diff --git a/type-plus/ts/tuple/tuple_type.is_tuple.spec.ts b/type-plus/ts/tuple/tuple_type.is_tuple.spec.ts index 54134bec01..95c16029a4 100644 --- a/type-plus/ts/tuple/tuple_type.is_tuple.spec.ts +++ b/type-plus/ts/tuple/tuple_type.is_tuple.spec.ts @@ -59,5 +59,5 @@ it('can override Then/Else', () => { }) it('can override never case', () => { - testType.equal, 3>(true) + testType.equal, 3>(true) }) diff --git a/type-plus/ts/tuple/tuple_type.not_tuple_type.spec.ts b/type-plus/ts/tuple/tuple_type.not_tuple_type.spec.ts index 1cf7596f3e..0f13edf7fb 100644 --- a/type-plus/ts/tuple/tuple_type.not_tuple_type.spec.ts +++ b/type-plus/ts/tuple/tuple_type.not_tuple_type.spec.ts @@ -59,5 +59,5 @@ it('can override Then/Else', () => { }) it('can override never case', () => { - testType.equal, 3>(true) + testType.equal, 3>(true) }) diff --git a/type-plus/ts/tuple/tuple_type.ts b/type-plus/ts/tuple/tuple_type.ts index d098bc0e5b..53cbeed349 100644 --- a/type-plus/ts/tuple/tuple_type.ts +++ b/type-plus/ts/tuple/tuple_type.ts @@ -25,7 +25,7 @@ export type TupleType< Options extends TupleType.Options = TupleType.DefaultOptions > = IsNever< T, - Options['caseNever'], + Options['$never'], [T] extends [readonly any[]] ? (number extends T['length'] ? Else : Then) : Else > @@ -34,7 +34,7 @@ export namespace TupleType { } export interface DefaultOptions { - caseNever: Else + $never: Else } } diff --git a/type-plus/ts/tuple/tuple_type.tuple_type.spec.ts b/type-plus/ts/tuple/tuple_type.tuple_type.spec.ts index 0286d08f5c..8ce15c80af 100644 --- a/type-plus/ts/tuple/tuple_type.tuple_type.spec.ts +++ b/type-plus/ts/tuple/tuple_type.tuple_type.spec.ts @@ -59,5 +59,5 @@ it('can override Then/Else', () => { }) it('can override never case', () => { - testType.equal, 3>(true) + testType.equal, 3>(true) }) diff --git a/type-plus/ts/type_plus/cases.ts b/type-plus/ts/type_plus/cases.ts new file mode 100644 index 0000000000..960913809e --- /dev/null +++ b/type-plus/ts/type_plus/cases.ts @@ -0,0 +1,14 @@ +const $then = Symbol('then') +export type $Then = typeof $then + +const $else = Symbol('else') +export type $Else = typeof $else + +const $never = Symbol('never') +export type $Never = typeof $never + +const $error = Symbol('error') +export interface $Error { + type: typeof $error, + message: M +} diff --git a/type-plus/ts/utils/options.ts b/type-plus/ts/utils/options.ts index 402638d1d9..5d106faec0 100644 --- a/type-plus/ts/utils/options.ts +++ b/type-plus/ts/utils/options.ts @@ -19,7 +19,7 @@ export namespace TypePlusOptions { * sequence, selection, and iteration. */ export interface Selection { - caseThen?: unknown, - caseElse?: unknown, + $then?: unknown, + $else?: unknown, } }