From 1cadac267a3858aaca40576db40f787777f8d64f Mon Sep 17 00:00:00 2001 From: gvergnaud Date: Mon, 10 Jun 2024 20:56:07 +0200 Subject: [PATCH 1/4] feat(narrow): add .narrow() method on Match --- docs/roadmap.md | 4 ++++ src/match.ts | 4 ++++ src/types/Match.ts | 17 ++++++++++++++--- tests/narrow.test.ts | 15 ++++++++++++++- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 0fa5de3a..52444ed3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,9 @@ ### Roadmap +- [ ] better variant attempt. +- [ ] `.narrow()` as an opt-in option. + - Can we avoid generating a distributed when pattern-matching on a single union type? + - In BuildMany/Distribute, we could probably check the number of unions that have been touched, and if it's one we don't distribute? - [ ] `P.array.includes(x)` - [ ] `P.record({Pkey}, {Pvalue})` - [x] `P.nonNullable` diff --git a/src/match.ts b/src/match.ts index 278aefda..17f21e9f 100644 --- a/src/match.ts +++ b/src/match.ts @@ -133,4 +133,8 @@ class MatchExpression { returnType() { return this; } + + narrow() { + return this; + } } diff --git a/src/types/Match.ts b/src/types/Match.ts index 77a27086..9bff234a 100644 --- a/src/types/Match.ts +++ b/src/types/Match.ts @@ -190,7 +190,7 @@ export type Match< * * [Read the documentation for `.exhaustive()` on GitHub](https://github.com/gvergnaud/ts-pattern#exhaustive) * - * */ + **/ exhaustive: DeepExcludeAll extends infer remainingCases ? [remainingCases] extends [never] ? () => PickReturnValue @@ -201,17 +201,28 @@ export type Match< * `.run()` return the resulting value. * * ⚠️ calling this function is unsafe, and may throw if no pattern matches your input. - * */ + **/ run(): PickReturnValue; /** * `.returnType()` Lets you specify the return type for all of your branches. * * [Read the documentation for `.returnType()` on GitHub](https://github.com/gvergnaud/ts-pattern#returnType) - * */ + **/ returnType: [inferredOutput] extends [never] ? () => Match : TSPatternError<'calling `.returnType()` is only allowed directly after `match(...)`.'>; + + /** + * `.narrow()` narrows the input type to exclude all cases that have previously been handled. + * + * `.narrow()` is only useful if you want to excluded cases from union types or nullable + * properties that are deeply nested. Handled cases from top level union types are excluded + * by default. + * + * [Read the documentation for `.narrow() on GitHub](https://github.com/gvergnaud/ts-pattern#narrow) + **/ + narrow(): Match, o, [], inferredOutput>; }; /** diff --git a/tests/narrow.test.ts b/tests/narrow.test.ts index 2e58ec1c..3bfdcd29 100644 --- a/tests/narrow.test.ts +++ b/tests/narrow.test.ts @@ -1,4 +1,4 @@ -import { P } from '../src'; +import { P, match } from '../src'; import { Equal, Expect } from '../src/types/helpers'; describe('P.narrow', () => { @@ -11,3 +11,16 @@ describe('P.narrow', () => { type test = Expect>; }); }); + +describe('.narrow() method', () => { + it('should excluded values from deeply nested union types.', () => { + const fn = (input: { prop?: string }) => + match(input) + .with({ prop: P.nullish }, () => false) + .narrow() + .otherwise(({ prop }) => { + type test = Expect>; + return true; + }); + }); +}); From 9b041b32db0fd2b128893e4e0ea0ee2a1de4ceac Mon Sep 17 00:00:00 2001 From: gvergnaud Date: Mon, 10 Jun 2024 21:24:55 +0200 Subject: [PATCH 2/4] feat(narrow): Avoid distributing unions when unnecessary --- docs/roadmap.md | 3 +- src/types/BuildMany.ts | 36 ++++-- src/types/DeepExclude.ts | 61 ++++++++- src/types/DistributeUnions.ts | 53 ++++---- tests/deep-exclude.test.ts | 221 ++++++++++++++------------------ tests/distribute-unions.test.ts | 53 ++++++++ tests/find-selected.test.ts | 20 ++- tests/invert-pattern.test.ts | 1 + 8 files changed, 277 insertions(+), 171 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 52444ed3..bc69bd08 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -3,7 +3,8 @@ - [ ] better variant attempt. - [ ] `.narrow()` as an opt-in option. - Can we avoid generating a distributed when pattern-matching on a single union type? - - In BuildMany/Distribute, we could probably check the number of unions that have been touched, and if it's one we don't distribute? + - In BuildMany/Distribute, we could probably check the number of unions that have been touched, and if it's one we don't distribute? + - => this creates performance issue. TODO: run trace analyzer. - [ ] `P.array.includes(x)` - [ ] `P.record({Pkey}, {Pvalue})` - [x] `P.nonNullable` diff --git a/src/types/BuildMany.ts b/src/types/BuildMany.ts index 4968c819..20ba36c3 100644 --- a/src/types/BuildMany.ts +++ b/src/types/BuildMany.ts @@ -12,28 +12,48 @@ type BuildOne = xs extends [ [infer value, infer path], ...infer tail ] - ? BuildOne>, tail> + ? BuildOne, tail> : data; -// Update :: a -> b -> PropertyKey[] -> a -type Update = path extends readonly [ +// Update :: a -> PropertyKey[] -> b +export type GetDeep = path extends readonly [ infer head, ...infer tail ] ? data extends readonly [any, ...any] ? head extends number - ? UpdateAt, Update> + ? GetDeep : never : data extends readonly (infer a)[] - ? Update[] + ? GetDeep : data extends Set - ? Set> + ? GetDeep : data extends Map - ? Map> + ? GetDeep + : head extends keyof data + ? GetDeep + : data + : data; + +// UpdateDeep :: a -> b -> PropertyKey[] -> a +export type UpdateDeep = path extends readonly [ + infer head, + ...infer tail +] + ? data extends readonly [any, ...any] + ? head extends number + ? UpdateAt, UpdateDeep> + : never + : data extends readonly (infer a)[] + ? UpdateDeep[] + : data extends Set + ? Set> + : data extends Map + ? Map> : head extends keyof data ? Compute< { [k in Exclude]: data[k] } & { - [k in head]: Update; + [k in head]: UpdateDeep; } > : data diff --git a/src/types/DeepExclude.ts b/src/types/DeepExclude.ts index fba5911a..e5338e94 100644 --- a/src/types/DeepExclude.ts +++ b/src/types/DeepExclude.ts @@ -1,3 +1,60 @@ -import { DistributeMatchingUnions } from './DistributeUnions'; +import { GetDeep, UpdateDeep } from './BuildMany'; +import { DistributeMatchingUnions, FindUnionsMany } from './DistributeUnions'; -export type DeepExclude = Exclude, b>; +export type DeepExclude = FindUnionsMany extends [ + { path: infer path; cases: { value: infer union; subUnions: [] } } +] + ? ExcludePath + : Exclude, b>; + +type ExcludePath< + a, + b, + path, + union = GetDeep, + excluded = GetDeep, + value = Exclude +> = [value] extends [never] ? never : UpdateDeep; + +type test1 = FindUnionsMany<{ a: 1 | 2 }, { a: 1 }>; // => +type test2 = FindUnionsMany< + // ^? + { a: { b: { c: { d: 1 | 2 } } } }, + { a: { b: { c: { d: 1 } } } } +>; +type test3 = FindUnionsMany< + // ^? + { a: { b: { a: 'a' | 'b' } | { a: 'a' | 'd' } } }, + { a: { b: { a: 'a' } } } +>; +type test4 = FindUnionsMany< + // ^? + { a: { b: { a: 'a' | 'b' } } } | { a: { b: { a: 'a' | 'd' } } }, + { a: { b: { a: 'a' } } } +>; +type test5 = FindUnionsMany< + // ^? + { a: { b: { a: 'a' } } } | { a: { b: { a: 'b' } } }, + { a: { b: { a: 'a' } } } +>; +type test6 = FindUnionsMany; // => + +type exclude1 = ExcludePath<{ a: 1 | 2 }, { a: 1 }, ['a']>; // => +type exclude2 = ExcludePath< + // ^? + { a: { b: { c: { d: 1 | 2 } } } }, + { a: { b: { c: { d: 1 } } } }, + ['a', 'b', 'c', 'd'] +>; +type exclude3 = ExcludePath< + // ^? + { a: { b: { a: 'a' | 'b' } | { a: 'a' | 'd' } } }, + { a: { b: { a: 'a' } } }, + ['a', 'b', 'a'] +>; +type exclude4 = ExcludePath< + // ^? + number[], + [number, ...number[]], + [] +>; diff --git a/src/types/DistributeUnions.ts b/src/types/DistributeUnions.ts index e2bc25f9..4e70ba6e 100644 --- a/src/types/DistributeUnions.ts +++ b/src/types/DistributeUnions.ts @@ -11,6 +11,7 @@ import type { ValueOf, MaybeAddReadonly, IsStrictArray, + IsTuple, } from './helpers'; import { IsMatching } from './IsMatching'; @@ -38,9 +39,10 @@ import { IsMatching } from './IsMatching'; * type t2 = DistributeMatchingUnions<['a' | 'b', 1 | 2], ['a', unknown]>; * // => ['a', 1 | 2] | ['b', 1 | 2] */ -export type DistributeMatchingUnions = IsAny extends true - ? any - : BuildMany>>; +export type DistributeMatchingUnions = BuildMany< + a, + Distribute> +>; // FindUnionsMany :: a -> Union -> PropertyKey[] -> UnionConfig[] export type FindUnionsMany< @@ -142,19 +144,11 @@ export type FindUnions< * in this case we turn the input array `A[]` into `[] | [A, ...A[]]` * to remove one of these cases during DeepExclude. */ - p extends readonly [] | readonly [any, ...any] | readonly [...any, any] + IsTuple

extends true ? IsStrictArray> extends false ? [] : [ - MaybeAddReadonly< - | (a extends readonly [any, ...any] | readonly [...any, any] - ? never - : []) - | (p extends readonly [...any, any] - ? [...Extract, ValueOf] - : [ValueOf, ...Extract]), - IsReadonlyArray - > extends infer aUnion + ArrayToVariadicUnion extends infer aUnion ? { cases: aUnion extends any ? { @@ -179,17 +173,24 @@ export type FindUnions< > : []; +export type ArrayToVariadicUnion = MaybeAddReadonly< + | [] + | (excluded extends readonly [...any, any] + ? [...Extract, ValueOf] + : [ValueOf, ...Extract]), + IsReadonlyArray +>; + // Distribute :: UnionConfig[] -> Union<[a, path][]> -export type Distribute = - unions extends readonly [ - { cases: infer cases; path: infer path }, - ...infer tail - ] - ? cases extends { value: infer value; subUnions: infer subUnions } - ? [ - [value, path], - ...Distribute>, - ...Distribute - ] - : never - : []; +export type Distribute = unions extends readonly [ + { cases: infer cases; path: infer path }, + ...infer tail +] + ? cases extends { value: infer value; subUnions: infer subUnions } + ? [ + [value, path], + ...Distribute>, + ...Distribute + ] + : never + : []; diff --git a/tests/deep-exclude.test.ts b/tests/deep-exclude.test.ts index 4a252175..7e3313c8 100644 --- a/tests/deep-exclude.test.ts +++ b/tests/deep-exclude.test.ts @@ -1,11 +1,5 @@ import { DeepExclude } from '../src/types/DeepExclude'; -import { - DistributeMatchingUnions, - FindUnions, - FindUnionsMany, -} from '../src/types/DistributeUnions'; import { Primitives, Equal, Expect } from '../src/types/helpers'; -import { IsMatching } from '../src/types/IsMatching'; import { BigUnion, Option, State } from './types-catalog/utils'; type Colors = 'pink' | 'purple' | 'red' | 'yellow' | 'blue'; @@ -61,106 +55,91 @@ describe('DeepExclude', () => { }); it('should work with nested object and only distribute what is necessary', () => { - type x = DeepExclude<{ str: string | null | undefined }, { str: string }>; - type xx = DistributeMatchingUnions< + type res1 = DeepExclude< { str: string | null | undefined }, { str: string } >; - type xxx = FindUnionsMany< + type test1 = Expect>; + + type res2 = DeepExclude< { str: string | null | undefined }, - { str: string } + { str: null | undefined } >; - type xxxx = IsMatching< - { str: string | null | undefined }, - { str: string } + type test2 = Expect>; + + type test3 = Expect< + Equal< + DeepExclude<{ a: { b: 'x' | 'y' } }, { a: { b: 'x' } }>, + { a: { b: 'y' } } + > >; - type xxxxx = FindUnions< - { str: string | null | undefined }, - { str: string }, - [] + + type res4 = DeepExclude<{ a: { b: 'x' | 'y' | 'z' } }, { a: { b: 'x' } }>; + type test4 = Expect>; + + type res5 = DeepExclude< + { a: { b: 'x' | 'y' | 'z' }; c: 'u' | 'v' }, + { a: { b: 'x' } } >; - type y = DeepExclude< - { str: string | null | undefined }, - { str: null | undefined } + type test5 = Expect>; + + type test6 = Expect< + Equal< + DeepExclude<{ a: { b: 'x' | 'y' | 'z' }; c: 'u' | 'v' }, { c: 'u' }>, + { a: { b: 'x' | 'y' | 'z' }; c: 'v' } + > >; - type cases = [ - Expect>, - Expect>, - Expect< - Equal< - DeepExclude<{ a: { b: 'x' | 'y' } }, { a: { b: 'x' } }>, - { a: { b: 'y' } } - > - >, - Expect< - Equal< - DeepExclude<{ a: { b: 'x' | 'y' | 'z' } }, { a: { b: 'x' } }>, - { a: { b: 'y' } } | { a: { b: 'z' } } - > - >, - Expect< - Equal< - DeepExclude< - { a: { b: 'x' | 'y' | 'z' }; c: 'u' | 'v' }, - { a: { b: 'x' } } - >, - { a: { b: 'y' }; c: 'u' | 'v' } | { a: { b: 'z' }; c: 'u' | 'v' } - > - >, - Expect< - Equal< - DeepExclude< - { a: { b: 'x' | 'y' | 'z' }; c: 'u' | 'v' }, - { c: 'u' } - >, - { a: { b: 'x' | 'y' | 'z' }; c: 'v' } - > - >, - Expect< - Equal< - DeepExclude< - { a: { b: 'x' | 'y' | 'z' }; c: 'u' | 'v' }, - { c: 'u' } - >, - { a: { b: 'x' | 'y' | 'z' }; c: 'v' } - > + type test7 = Expect< + Equal< + DeepExclude<{ a: { b: 'x' | 'y' | 'z' }; c: 'u' | 'v' }, { c: 'u' }>, + { a: { b: 'x' | 'y' | 'z' }; c: 'v' } > - ]; + >; }); }); describe('Tuples', () => { it('should correctly exclude when it matches', () => { - type cases = [ - Expect, never>>, - Expect, ['y']>>, - Expect< - Equal< - DeepExclude<[string, string], readonly [unknown, unknown]>, - never - > - >, - Expect< - Equal< - DeepExclude<[number, State], [unknown, { status: 'error' }]>, - | [number, { status: 'idle' }] - | [number, { status: 'loading' }] - | [number, { status: 'success'; data: string }] - > - >, - Expect< - Equal< - DeepExclude< - readonly [number, State], - [unknown, { status: 'error' }] - >, - | [number, { status: 'idle' }] - | [number, { status: 'loading' }] - | [number, { status: 'success'; data: string }] - > + type res1 = DeepExclude<['x' | 'y'], [string]>; + type test1 = Expect>; + type test2 = Expect, ['y']>>; + type test3 = Expect< + Equal, never> + >; + + type res4 = DeepExclude<[number, State], [unknown, { status: 'error' }]>; + type type4 = Expect< + Equal< + res4, + [ + number, + ( + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: string } + ) + ] > - ]; + >; + + type res5 = DeepExclude< + readonly [number, State], + [unknown, { status: 'error' }] + >; + type test5 = Expect< + Equal< + res5, + [ + number, + ( + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: string } + ) + ] + > + >; }); it("if it doesn't match, it should leave the data structure untouched", () => { @@ -172,24 +151,16 @@ describe('DeepExclude', () => { }); it('should work with nested tuples and only distribute what is necessary', () => { - type cases = [ - Expect, [['y']]>>, - Expect< - Equal, [['y']] | [['z']]> - >, - Expect< - Equal< - DeepExclude<[['x' | 'y' | 'z'], 'u' | 'v'], [['x'], unknown]>, - [['y'], 'u' | 'v'] | [['z'], 'u' | 'v'] - > - >, - Expect< - Equal< - DeepExclude<[['x' | 'y' | 'z'], 'u' | 'v'], [unknown, 'v']>, - [['x' | 'y' | 'z'], 'u'] - > - > - ]; + type test1 = Expect, [['y']]>>; + + type res2 = DeepExclude<[['x' | 'y' | 'z']], [['x']]>; + type test2 = Expect>; + + type res3 = DeepExclude<[['x' | 'y' | 'z'], 'u' | 'v'], [['x'], unknown]>; + type test3 = Expect>; + + type res4 = DeepExclude<[['x' | 'y' | 'z'], 'u' | 'v'], [unknown, 'v']>; + type test4 = Expect>; }); it('should work with nested unary tuples', () => { @@ -271,21 +242,14 @@ describe('DeepExclude', () => { Equal >; - type cases = [ - Expect, [1, 2, 3]>>, - Expect< - Equal< - DeepExclude<{ values: [] | [1, 2, 3] }, { values: [] }>, - { values: [1, 2, 3] } - > - >, - Expect< - Equal< - DeepExclude<{ values: [1, 2, 3] }, { values: [] }>, - { values: [1, 2, 3] } - > - > - ]; + type res2 = DeepExclude<[] | [1, 2, 3], []>; + type test2 = Expect>; + + type res3 = DeepExclude<{ values: [] | [1, 2, 3] }, { values: [] }>; + type test3 = Expect>; + + type res4 = DeepExclude<{ values: [1, 2, 3] }, { values: [] }>; + type test4 = Expect>; }); }); @@ -580,4 +544,15 @@ describe('DeepExclude', () => { > >; }); + + describe('should not distribute when a single union is matched', () => { + type res1 = DeepExclude; + type test1 = Expect>; + + type res2 = DeepExclude< + readonly [1 | 2 | 3, 'c' | 'd'] | [2 | 3, 'c' | 'e'], + [1, 'c'] + >; + type test2 = Expect>; + }); }); diff --git a/tests/distribute-unions.test.ts b/tests/distribute-unions.test.ts index 1574c83c..b69e5ef0 100644 --- a/tests/distribute-unions.test.ts +++ b/tests/distribute-unions.test.ts @@ -560,6 +560,59 @@ describe('FindAllUnions', () => { > ]; }); + + it('should find whole tree structures when a pattern matches several unions on its way to the deepest value', () => { + type input1 = { b: { a: 'a' | 'b' } | { a: 'a' | 'd' } }; + type match1 = { b: { a: 'a' } }; + type res1 = FindUnionsMany; + + type expected1 = [ + { + cases: + | { + value: { + a: 'a' | 'b'; + }; + subUnions: [ + { + cases: + | { + value: 'a'; + subUnions: []; + } + | { + value: 'b'; + subUnions: []; + }; + path: ['b', 'a']; + } + ]; + } + | { + value: { + a: 'a' | 'd'; + }; + subUnions: [ + { + cases: + | { + value: 'a'; + subUnions: []; + } + | { + value: 'd'; + subUnions: []; + }; + path: ['b', 'a']; + } + ]; + }; + path: ['b']; + } + ]; + + type test1 = Expect>; + }); }); describe('Distribute', () => { diff --git a/tests/find-selected.test.ts b/tests/find-selected.test.ts index 41d9ba89..dcb90808 100644 --- a/tests/find-selected.test.ts +++ b/tests/find-selected.test.ts @@ -340,18 +340,16 @@ describe('FindSelected', () => { }); it('should return an error when trying to use several anonymous select', () => { + type res1 = FindSelected< + // ^? + { a: [{ c: 3 }, { e: 7 }]; b: { d: string }[] }, + { + a: [{ c: AnonymousSelectP }, { e: AnonymousSelectP }]; + } + >; + type cases = [ - Expect< - Equal< - FindSelected< - { a: [{ c: 3 }, { e: 7 }]; b: { d: string }[] }, - { - a: [{ c: AnonymousSelectP }, { e: AnonymousSelectP }]; - } - >, - SeveralAnonymousSelectError - > - >, + Expect>, Expect< Equal< FindSelected< diff --git a/tests/invert-pattern.test.ts b/tests/invert-pattern.test.ts index 2ba60f9e..4adc1fba 100644 --- a/tests/invert-pattern.test.ts +++ b/tests/invert-pattern.test.ts @@ -1,3 +1,4 @@ +import { P } from '../src'; import { Equal, Expect } from '../src/types/helpers'; import { InvertPattern, From 273c223ad34e63fb9a2722f59d9b4e29a576d7f2 Mon Sep 17 00:00:00 2001 From: gvergnaud Date: Tue, 11 Jun 2024 20:28:39 -0400 Subject: [PATCH 3/4] feat(narrow): Fix performance problem --- docs/roadmap.md | 7 ++-- src/types/BuildMany.ts | 50 +++++++++++++-------------- src/types/DeepExclude.ts | 65 ++++++----------------------------- src/types/DistributeUnions.ts | 6 +--- tests/narrow.test.ts | 10 ++++++ 5 files changed, 49 insertions(+), 89 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index bc69bd08..c2e4cffc 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,10 +1,9 @@ ### Roadmap - [ ] better variant attempt. -- [ ] `.narrow()` as an opt-in option. - - Can we avoid generating a distributed when pattern-matching on a single union type? - - In BuildMany/Distribute, we could probably check the number of unions that have been touched, and if it's one we don't distribute? - - => this creates performance issue. TODO: run trace analyzer. +- [x] `.narrow()` as an opt-in option. + - [ ] maybe add a `.narrowDeep()` + - [ ] Try making this behavior the default - [ ] `P.array.includes(x)` - [ ] `P.record({Pkey}, {Pvalue})` - [x] `P.nonNullable` diff --git a/src/types/BuildMany.ts b/src/types/BuildMany.ts index 20ba36c3..379d3ee6 100644 --- a/src/types/BuildMany.ts +++ b/src/types/BuildMany.ts @@ -1,4 +1,4 @@ -import { Compute, Iterator, UpdateAt } from './helpers'; +import { Iterator, UpdateAt, ValueOf } from './helpers'; // BuildMany :: DataStructure -> Union<[value, path][]> -> Union export type BuildMany = xs extends any @@ -12,49 +12,49 @@ type BuildOne = xs extends [ [infer value, infer path], ...infer tail ] - ? BuildOne, tail> + ? BuildOne, tail> : data; -// Update :: a -> PropertyKey[] -> b +// GetDeep :: a -> PropertyKey[] -> b export type GetDeep = path extends readonly [ infer head, ...infer tail ] - ? data extends readonly [any, ...any] - ? head extends number - ? GetDeep - : never - : data extends readonly (infer a)[] - ? GetDeep + ? data extends readonly any[] + ? data extends readonly [any, ...any] + ? head extends number + ? GetDeep + : never + : GetDeep, tail> : data extends Set ? GetDeep - : data extends Map + : data extends Map ? GetDeep : head extends keyof data ? GetDeep : data : data; -// UpdateDeep :: a -> b -> PropertyKey[] -> a -export type UpdateDeep = path extends readonly [ +// SetDeep :: a -> b -> PropertyKey[] -> a +export type SetDeep = path extends readonly [ infer head, ...infer tail ] - ? data extends readonly [any, ...any] - ? head extends number - ? UpdateAt, UpdateDeep> - : never - : data extends readonly (infer a)[] - ? UpdateDeep[] + ? data extends readonly any[] + ? data extends readonly [any, ...any] + ? head extends number + ? UpdateAt, SetDeep> + : never + : SetDeep, value, tail>[] : data extends Set - ? Set> + ? Set> : data extends Map - ? Map> + ? Map> : head extends keyof data - ? Compute< - { [k in Exclude]: data[k] } & { - [k in head]: UpdateDeep; - } - > + ? { + [k in keyof data]-?: k extends head + ? SetDeep + : data[k]; + } : data : value; diff --git a/src/types/DeepExclude.ts b/src/types/DeepExclude.ts index e5338e94..bbea1d6f 100644 --- a/src/types/DeepExclude.ts +++ b/src/types/DeepExclude.ts @@ -1,60 +1,15 @@ -import { GetDeep, UpdateDeep } from './BuildMany'; +import { GetDeep, SetDeep } from './BuildMany'; import { DistributeMatchingUnions, FindUnionsMany } from './DistributeUnions'; export type DeepExclude = FindUnionsMany extends [ - { path: infer path; cases: { value: infer union; subUnions: [] } } + { + path: infer path; + cases: { value: infer union; subUnions: [] }; + } ] - ? ExcludePath + ? Exclude> extends infer narrowed + ? [narrowed] extends [never] + ? never + : SetDeep + : never : Exclude, b>; - -type ExcludePath< - a, - b, - path, - union = GetDeep, - excluded = GetDeep, - value = Exclude -> = [value] extends [never] ? never : UpdateDeep; - -type test1 = FindUnionsMany<{ a: 1 | 2 }, { a: 1 }>; // => -type test2 = FindUnionsMany< - // ^? - { a: { b: { c: { d: 1 | 2 } } } }, - { a: { b: { c: { d: 1 } } } } ->; -type test3 = FindUnionsMany< - // ^? - { a: { b: { a: 'a' | 'b' } | { a: 'a' | 'd' } } }, - { a: { b: { a: 'a' } } } ->; -type test4 = FindUnionsMany< - // ^? - { a: { b: { a: 'a' | 'b' } } } | { a: { b: { a: 'a' | 'd' } } }, - { a: { b: { a: 'a' } } } ->; -type test5 = FindUnionsMany< - // ^? - { a: { b: { a: 'a' } } } | { a: { b: { a: 'b' } } }, - { a: { b: { a: 'a' } } } ->; -type test6 = FindUnionsMany; // => - -type exclude1 = ExcludePath<{ a: 1 | 2 }, { a: 1 }, ['a']>; // => -type exclude2 = ExcludePath< - // ^? - { a: { b: { c: { d: 1 | 2 } } } }, - { a: { b: { c: { d: 1 } } } }, - ['a', 'b', 'c', 'd'] ->; -type exclude3 = ExcludePath< - // ^? - { a: { b: { a: 'a' | 'b' } | { a: 'a' | 'd' } } }, - { a: { b: { a: 'a' } } }, - ['a', 'b', 'a'] ->; -type exclude4 = ExcludePath< - // ^? - number[], - [number, ...number[]], - [] ->; diff --git a/src/types/DistributeUnions.ts b/src/types/DistributeUnions.ts index 4e70ba6e..30ad9763 100644 --- a/src/types/DistributeUnions.ts +++ b/src/types/DistributeUnions.ts @@ -187,10 +187,6 @@ export type Distribute = unions extends readonly [ ...infer tail ] ? cases extends { value: infer value; subUnions: infer subUnions } - ? [ - [value, path], - ...Distribute>, - ...Distribute - ] + ? [[value, path], ...Distribute, ...Distribute] : never : []; diff --git a/tests/narrow.test.ts b/tests/narrow.test.ts index 3bfdcd29..c5e930da 100644 --- a/tests/narrow.test.ts +++ b/tests/narrow.test.ts @@ -22,5 +22,15 @@ describe('.narrow() method', () => { type test = Expect>; return true; }); + + const fn2 = (input: { prop?: 1 | 2 | 3 }) => + match(input) + .with({ prop: P.nullish }, () => false) + .with({ prop: 2 }, () => false) + .narrow() + .otherwise(({ prop }) => { + type test = Expect>; + return true; + }); }); }); From 6833be9853a9feb31454f9f7dbd596165fe04138 Mon Sep 17 00:00:00 2001 From: gvergnaud Date: Thu, 20 Jun 2024 14:16:38 -0400 Subject: [PATCH 4/4] feat(narrow): Fix IsSingleUnion logic --- docs/roadmap.md | 3 ++- src/types/BuildMany.ts | 2 +- src/types/DeepExclude.ts | 26 ++++++++++++++------------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index c2e4cffc..c0ca7634 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,8 +2,9 @@ - [ ] better variant attempt. - [x] `.narrow()` as an opt-in option. + - [ ] Try making single union deep narrowing the default - [ ] maybe add a `.narrowDeep()` - - [ ] Try making this behavior the default + - [x] Try making this behavior the default: too slow - [ ] `P.array.includes(x)` - [ ] `P.record({Pkey}, {Pvalue})` - [x] `P.nonNullable` diff --git a/src/types/BuildMany.ts b/src/types/BuildMany.ts index 379d3ee6..0b43c765 100644 --- a/src/types/BuildMany.ts +++ b/src/types/BuildMany.ts @@ -32,7 +32,7 @@ export type GetDeep = path extends readonly [ ? GetDeep : head extends keyof data ? GetDeep - : data + : never : data; // SetDeep :: a -> b -> PropertyKey[] -> a diff --git a/src/types/DeepExclude.ts b/src/types/DeepExclude.ts index bbea1d6f..fd9d0d91 100644 --- a/src/types/DeepExclude.ts +++ b/src/types/DeepExclude.ts @@ -1,15 +1,17 @@ import { GetDeep, SetDeep } from './BuildMany'; import { DistributeMatchingUnions, FindUnionsMany } from './DistributeUnions'; -export type DeepExclude = FindUnionsMany extends [ - { - path: infer path; - cases: { value: infer union; subUnions: [] }; - } -] - ? Exclude> extends infer narrowed - ? [narrowed] extends [never] - ? never - : SetDeep - : never - : Exclude, b>; +export type DeepExclude = + // If a single union is found + FindUnionsMany extends [ + { path: infer path; cases: infer cases & { subUnions: [] } } + ] + ? Exclude< + GetDeep, + GetDeep + > extends infer narrowed + ? [narrowed] extends [never] + ? never + : SetDeep + : never + : Exclude, b>;