Skip to content

Commit 4baac7e

Browse files
update: refine ExcludeExactly, update test-cases.
Co-authored-by: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com>
1 parent 8b984c9 commit 4baac7e

File tree

2 files changed

+35
-85
lines changed

2 files changed

+35
-85
lines changed

source/exclude-exactly.d.ts

Lines changed: 29 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,7 @@
1-
import type {IsUnknown} from './is-unknown.d.ts';
21
import type {IsNever} from './is-never.d.ts';
32
import type {IsAny} from './is-any.d.ts';
4-
import type {LastOfUnion} from './last-of-union.d.ts';
5-
6-
/**
7-
Return `never` if the first and second arguments are identical.
8-
Return the first argument if not.
9-
(But there's a limitation about union/intersection type. See `IsEqual` in `source/is-equal.d.ts`.)
10-
11-
@example
12-
```
13-
type A = MatchOrNever<string | number, string>;
14-
//=> string | number
15-
type B = MatchOrNever<string | number, string | number>;
16-
//=> never
17-
type C = MatchOrNever<string | number, unknown>;
18-
//=> string | number
19-
type D = MatchOrNever<string, string | number>;
20-
//=> string
21-
```
22-
23-
This does NOT depend on assignability.
24-
25-
@example
26-
```
27-
type RO_0 = MatchOrNever<{readonly a: 0}, {a: 0}>;
28-
//=> {readonly a: 0}
29-
type RO_1 = MatchOrNever<{a: 0}, {readonly a: 0}>;
30-
//=> {a: 0}
31-
```
32-
33-
`unknown` and `never` cases, which easily break equality in type-level codebase.
34-
35-
@example
36-
```
37-
type E = MatchOrNever<unknown, never>;
38-
//=> unknown
39-
type F = MatchOrNever<unknown, unknown>;
40-
//=> never
41-
type G = MatchOrNever<never, never>;
42-
//=> never
43-
type H = MatchOrNever<never, unknown>;
44-
//=> never
45-
```
46-
47-
Note that this doesn't regard the identical union/intersection type `T | T` and/or `T & T` as `T` recursively.
48-
e.g., `{a: 0} | {a: 0}` and/or `{a: 0} & {a: 0}` as `{a: 0}`.
49-
50-
@example
51-
```
52-
type IDUnion = MatchOrNever<{a: {b: 0}} | {a: {b: 0}}, {a: {b: 0}}>;
53-
//=> never
54-
type A = {a: {b: 0} | {b: 0}};
55-
type RecurivelyIDUnion = MatchOrNever<A, {a: {b: 0}}>;
56-
//=> A
57-
```
58-
*/
59-
type MatchOrNever<A, B> =
60-
[unknown, B] extends [A, never]
61-
? A
62-
// This equality code base below doesn't work if `A` is `unknown` and `B` is `never` case.
63-
// So this branch should be wrapped to take care of this.
64-
: (<G>() => G extends A & G | G ? 1 : 2) extends (<G>() => G extends B & G | G ? 1 : 2)
65-
? never
66-
: A;
3+
import type {If} from './if.d.ts';
4+
import type {IfNotAnyOrNever} from './internal/type.d.ts';
675

686
/**
697
A stricter version of `Exclude<T, U>` that ensures objects with different key modifiers are not considered identical.
@@ -106,19 +44,32 @@ type ExcludeFromUnknownArray = ExcludeExactly<number[] | unknown[], number[]>;
10644
10745
@category Improved Built-in
10846
*/
109-
export type ExcludeExactly<UnionU, DeleteT> =
110-
LastOfUnion<DeleteT> extends infer D
111-
? true extends IsNever<D>
112-
? UnionU
113-
: ExcludeExactly<_ExcludeExactly<UnionU, D>, _ExcludeExactly<DeleteT, D>>
114-
: never;
47+
export type ExcludeExactly<Union, Delete> =
48+
IfNotAnyOrNever<
49+
Union,
50+
_ExcludeExactly<Union, Delete>,
51+
// If `Union` is `any`, then if `Delete` is `any`, return `never`, else return `Union`.
52+
If<IsAny<Delete>, never, Union>,
53+
// If `Union` is `never`, then if `Delete` is `never`, return `never`, else return `Union`.
54+
If<IsNever<Delete>, never, Union>
55+
>;
56+
57+
type _ExcludeExactly<Union, Delete> =
58+
IfNotAnyOrNever<Delete,
59+
Union extends unknown // For distributing `Union`
60+
? [Delete extends unknown // For distributing `Delete`
61+
? If<SimpleIsEqual<Union, Delete>, true, never>
62+
: never] extends [never] ? Union : never
63+
: never,
64+
// If `Delete` is `any` or `never`, then return `Union`,
65+
// because `Union` cannot be `any` or `never` here.
66+
Union, Union
67+
>;
68+
69+
type SimpleIsEqual<A, B> =
70+
(<G>() => G extends A & G | G ? 1 : 2) extends
71+
(<G>() => G extends B & G | G ? 1 : 2)
72+
? true
73+
: false;
11574

116-
type _ExcludeExactly<UnionU, DeleteT> =
117-
true extends IsAny<DeleteT>
118-
? never
119-
: true extends IsUnknown<DeleteT>
120-
? never
121-
: UnionU extends unknown // Only for union distribution.
122-
? MatchOrNever<UnionU, DeleteT>
123-
: never;
12475
export {};

test-d/exclude-exactly.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {ExcludeExactly} from '../index.d.ts';
33

44
expectType<number>({} as ExcludeExactly<0 | 1 | number, '1'>);
55
expectType<never>({} as ExcludeExactly<0 | 1 | number, number>);
6+
expectType<0>({} as ExcludeExactly<0, number>);
67
expectType<string>({} as ExcludeExactly<'0' | '1' | string, '1'>);
78
expectType<never>({} as ExcludeExactly<'0' | '1' | string, string>);
89

@@ -22,15 +23,13 @@ expectType<unknown[]>({} as ExcludeExactly<unknown[], number[]>);
2223
expectType<{a: unknown}>({} as ExcludeExactly<{a: unknown}, {a: number}>);
2324
expectType<unknown[]>({} as ExcludeExactly<number[] | unknown[], number[]>);
2425

25-
// `unknown` and `any` exclude themselves.
26+
// `unknown` excludes `unknown`, `any` excludes `any`.
2627
expectType<never>({} as ExcludeExactly<unknown, unknown>);
27-
expectType<never>({} as ExcludeExactly<unknown, any>);
28+
expectType<unknown>({} as ExcludeExactly<unknown, any>);
2829
expectType<never>({} as ExcludeExactly<any, any>);
29-
expectType<never>({} as ExcludeExactly<any, unknown>);
30-
31-
// `unknown` and `any` exclude other types.
32-
expectType<never>({} as ExcludeExactly<string | number, unknown>);
33-
expectType<never>({} as ExcludeExactly<string | number, any>);
30+
expectType<any>({} as ExcludeExactly<any, unknown>);
31+
expectType<string | number>({} as ExcludeExactly<string | number, unknown>);
32+
expectType<string | number>({} as ExcludeExactly<string | number, any>);
3433

3534
// Union
3635
expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>);

0 commit comments

Comments
 (0)