Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: .narrow() method & typechecking perf improvement #261

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
### Roadmap

- [ ] better variant attempt.
- [x] `.narrow()` as an opt-in option.
- [ ] Try making single union deep narrowing the default
- [ ] maybe add a `.narrowDeep()`
- [x] Try making this behavior the default: too slow
- [ ] `P.array.includes(x)`
- [ ] `P.record({Pkey}, {Pvalue})`
- [x] `P.nonNullable`
Expand Down
4 changes: 4 additions & 0 deletions src/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,8 @@ class MatchExpression<input, output> {
returnType() {
return this;
}

narrow() {
return this;
}
}
54 changes: 37 additions & 17 deletions src/types/BuildMany.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Compute, Iterator, UpdateAt } from './helpers';
import { Iterator, UpdateAt, ValueOf } from './helpers';

// BuildMany :: DataStructure -> Union<[value, path][]> -> Union<DataStructure>
export type BuildMany<data, xs extends readonly any[]> = xs extends any
Expand All @@ -12,29 +12,49 @@ type BuildOne<data, xs extends readonly any[]> = xs extends [
[infer value, infer path],
...infer tail
]
? BuildOne<Update<data, value, Extract<path, readonly PropertyKey[]>>, tail>
? BuildOne<SetDeep<data, value, path>, tail>
: data;

// Update :: a -> b -> PropertyKey[] -> a
type Update<data, value, path> = path extends readonly [
// GetDeep :: a -> PropertyKey[] -> b
export type GetDeep<data, path> = path extends readonly [
infer head,
...infer tail
]
? data extends readonly [any, ...any]
? head extends number
? UpdateAt<data, Iterator<head>, Update<data[head], value, tail>>
: never
: data extends readonly (infer a)[]
? Update<a, value, tail>[]
? data extends readonly any[]
? data extends readonly [any, ...any]
? head extends number
? GetDeep<data[head], tail>
: never
: GetDeep<ValueOf<data>, tail>
: data extends Set<infer a>
? Set<Update<a, value, tail>>
? GetDeep<a, tail>
: data extends Map<any, infer v>
? GetDeep<v, tail>
: head extends keyof data
? GetDeep<data[head], tail>
: never
: data;

// SetDeep :: a -> b -> PropertyKey[] -> a
export type SetDeep<data, value, path> = path extends readonly [
infer head,
...infer tail
]
? data extends readonly any[]
? data extends readonly [any, ...any]
? head extends number
? UpdateAt<data, Iterator<head>, SetDeep<data[head], value, tail>>
: never
: SetDeep<ValueOf<data>, value, tail>[]
: data extends Set<infer a>
? Set<SetDeep<a, value, tail>>
: data extends Map<infer k, infer v>
? Map<k, Update<v, value, tail>>
? Map<k, SetDeep<v, value, tail>>
: head extends keyof data
? Compute<
{ [k in Exclude<keyof data, head>]: data[k] } & {
[k in head]: Update<data[k], value, tail>;
}
>
? {
[k in keyof data]-?: k extends head
? SetDeep<data[head], value, tail>
: data[k];
}
: data
: value;
18 changes: 16 additions & 2 deletions src/types/DeepExclude.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import { DistributeMatchingUnions } from './DistributeUnions';
import { GetDeep, SetDeep } from './BuildMany';
import { DistributeMatchingUnions, FindUnionsMany } from './DistributeUnions';

export type DeepExclude<a, b> = Exclude<DistributeMatchingUnions<a, b>, b>;
export type DeepExclude<a, b> =
// If a single union is found
FindUnionsMany<a, b> extends [
{ path: infer path; cases: infer cases & { subUnions: [] } }
]
? Exclude<
GetDeep<cases, ['value']>,
GetDeep<b, path>
> extends infer narrowed
? [narrowed] extends [never]
? never
: SetDeep<a, narrowed, path>
: never
: Exclude<DistributeMatchingUnions<a, b>, b>;
49 changes: 23 additions & 26 deletions src/types/DistributeUnions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
ValueOf,
MaybeAddReadonly,
IsStrictArray,
IsTuple,
} from './helpers';
import { IsMatching } from './IsMatching';

Expand Down Expand Up @@ -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<a, p> = IsAny<a> extends true
? any
: BuildMany<a, Distribute<FindUnionsMany<a, p>>>;
export type DistributeMatchingUnions<a, p> = BuildMany<
a,
Distribute<FindUnionsMany<a, p>>
>;

// FindUnionsMany :: a -> Union<a> -> PropertyKey[] -> UnionConfig[]
export type FindUnionsMany<
Expand Down Expand Up @@ -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<p> extends true
? IsStrictArray<Extract<a, readonly any[]>> extends false
? []
: [
MaybeAddReadonly<
| (a extends readonly [any, ...any] | readonly [...any, any]
? never
: [])
| (p extends readonly [...any, any]
? [...Extract<a, readonly any[]>, ValueOf<a>]
: [ValueOf<a>, ...Extract<a, readonly any[]>]),
IsReadonlyArray<a>
> extends infer aUnion
ArrayToVariadicUnion<a, p> extends infer aUnion
? {
cases: aUnion extends any
? {
Expand All @@ -179,17 +173,20 @@ export type FindUnions<
>
: [];

export type ArrayToVariadicUnion<input, excluded> = MaybeAddReadonly<
| []
| (excluded extends readonly [...any, any]
? [...Extract<input, readonly any[]>, ValueOf<input>]
: [ValueOf<input>, ...Extract<input, readonly any[]>]),
IsReadonlyArray<input>
>;

// Distribute :: UnionConfig[] -> Union<[a, path][]>
export type Distribute<unions extends readonly any[]> =
unions extends readonly [
{ cases: infer cases; path: infer path },
...infer tail
]
? cases extends { value: infer value; subUnions: infer subUnions }
? [
[value, path],
...Distribute<Extract<subUnions, readonly any[]>>,
...Distribute<tail>
]
: never
: [];
export type Distribute<unions> = unions extends readonly [
{ cases: infer cases; path: infer path },
...infer tail
]
? cases extends { value: infer value; subUnions: infer subUnions }
? [[value, path], ...Distribute<subUnions>, ...Distribute<tail>]
: never
: [];
17 changes: 14 additions & 3 deletions src/types/Match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export type Match<
*
* [Read the documentation for `.exhaustive()` on GitHub](https://github.com/gvergnaud/ts-pattern#exhaustive)
*
* */
**/
exhaustive: DeepExcludeAll<i, handledCases> extends infer remainingCases
? [remainingCases] extends [never]
? () => PickReturnValue<o, inferredOutput>
Expand All @@ -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<o, inferredOutput>;

/**
* `.returnType<T>()` 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]
? <output>() => Match<i, output, handledCases>
: TSPatternError<'calling `.returnType<T>()` 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<DeepExcludeAll<i, handledCases>, o, [], inferredOutput>;
};

/**
Expand Down
Loading