Skip to content

Commit e6f62a2

Browse files
authored
Add ExcludeStrict type (#1123)
1 parent 544a846 commit e6f62a2

File tree

4 files changed

+140
-0
lines changed

4 files changed

+140
-0
lines changed

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,4 @@ export type {TsConfigJson} from './source/tsconfig-json.d.ts';
179179

180180
// Improved built-in
181181
export type {ExtractStrict} from './source/extract-strict.d.ts';
182+
export type {ExcludeStrict} from './source/exclude-strict.d.ts';

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ type ShouldBeNever = IfAny<'not any', 'not never', 'never'>;
339339
### Improved built-in
340340

341341
- [`ExtractStrict`](source/extract-strict.d.ts) - A stricter version of `Extract<T, U>` that ensures every member of `U` can successfully extract something from `T`.
342+
- [`ExcludeStrict`](source/exclude-strict.d.ts) - A stricter version of `Exclude<T, U>` that ensures every member of `U` can successfully exclude something from `T`.
342343

343344
## Declined types
344345

source/exclude-strict.d.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
A stricter version of {@link Exclude<T, U>} that ensures every member of `U` can successfully exclude something from `T`.
3+
4+
For example, `ExcludeStrict<string | number | boolean, number | bigint>` will error because `bigint` cannot exclude anything from `string | number | boolean`.
5+
6+
@example
7+
```
8+
// Valid Examples
9+
10+
type Example1 = ExcludeStrict<{status: 'success'; data: string[]} | {status: 'error'; error: string}, {status: 'success'}>;
11+
//=> {status: 'error'; error: string}
12+
13+
type Example2 = ExcludeStrict<'xs' | 's' | 'm' | 'l' | 'xl', 'xs' | 's'>;
14+
//=> 'm' | 'l' | 'xl'
15+
16+
type Example3 = ExcludeStrict<{x: number; y: number} | [number, number], unknown[]>;
17+
//=> {x: number; y: number}
18+
```
19+
20+
@example
21+
```
22+
// Invalid Examples
23+
24+
// `'xxl'` cannot exclude anything from `'xs' | 's' | 'm' | 'l' | 'xl'`
25+
type Example1 = ExcludeStrict<'xs' | 's' | 'm' | 'l' | 'xl', 'xl' | 'xxl'>;
26+
// ~~~~~~~~~~~~
27+
// Error: Type "'xl' | 'xxl'" does not satisfy the constraint 'never'.
28+
29+
// `unknown[]` cannot exclude anything from `{x: number; y: number} | {x: string; y: string}`
30+
type Example2 = ExcludeStrict<{x: number; y: number} | {x: string; y: string}, unknown[]>;
31+
// ~~~~~~~~~
32+
// Error: Type 'unknown[]' does not satisfy the constraint 'never'.
33+
```
34+
35+
@category Improved Built-in
36+
*/
37+
export type ExcludeStrict<
38+
T,
39+
U extends [U] extends [
40+
// Ensure every member of `U` excludes something from `T`
41+
U extends unknown ? ([T] extends [Exclude<T, U>] ? never : U) : never,
42+
]
43+
? unknown
44+
: never,
45+
> = Exclude<T, U>;

test-d/exclude-strict.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {expectType} from 'tsd';
2+
import type {ExcludeStrict} from '../source/exclude-strict.js';
3+
4+
// Primitive union tests
5+
6+
type ShirtSize = 'xxxl' | 'xxl' | 'xl' | 'l' | 'm' | 's' | 'xs' | 'xxs';
7+
type LargeShirtSize = 'xxxl' | 'xxl' | 'xl' | 'l';
8+
type SmallShirtSize = 's' | 'xs' | 'xxs';
9+
10+
declare const nonLargeShirtSizes: ExcludeStrict<ShirtSize, LargeShirtSize>;
11+
expectType<'m' | SmallShirtSize>(nonLargeShirtSizes);
12+
13+
declare const nonSmallShirtSizes: ExcludeStrict<ShirtSize, SmallShirtSize>;
14+
expectType<LargeShirtSize | 'm'>(nonSmallShirtSizes);
15+
16+
// @ts-expect-error
17+
declare const allInvalidShirtSizes: ExcludeStrict<ShirtSize, 'skyscraper-large' | 'atom-small'>;
18+
19+
// @ts-expect-error
20+
declare const someInvalidShirtSizes: ExcludeStrict<ShirtSize, 'm' | 'atom-small'>;
21+
22+
// Object union tests
23+
24+
type Foo = {
25+
kind: 'foo';
26+
a: string;
27+
b: string;
28+
};
29+
30+
type Bar = {
31+
kind: 'bar';
32+
a: string;
33+
b: number;
34+
c: boolean;
35+
};
36+
37+
type Foobar = Foo | Bar;
38+
39+
expectType<never>({} as ExcludeStrict<Foobar, {a: string}>);
40+
expectType<Bar>({} as ExcludeStrict<Foobar, {kind: 'foo'}>);
41+
expectType<Bar>({} as ExcludeStrict<Foobar, {b: string}>);
42+
expectType<Foo>({} as ExcludeStrict<Foobar, {c: boolean}>);
43+
expectType<never>({} as ExcludeStrict<Foobar, {b: string} | {c: boolean}>);
44+
45+
// @ts-expect-error
46+
declare const invalidLoneField: ExcludeStrict<Foobar, {d: string}>;
47+
48+
// @ts-expect-error
49+
declare const invalidMixedFields: ExcludeStrict<Foobar, {kind: 'foo'; d: string}>;
50+
51+
// @ts-expect-error
52+
declare const undefinedField: ExcludeStrict<Foobar, undefined>;
53+
54+
// Primitives
55+
expectType<number>({} as ExcludeStrict<string | number, string>);
56+
expectType<string>({} as ExcludeStrict<string | number | bigint, number | bigint>);
57+
expectType<'foo'>({} as ExcludeStrict<'foo' | 'bar' | 'baz', `b${string}`>);
58+
59+
// @ts-expect-error
60+
type invalid1 = ExcludeStrict<string | number | boolean, number | bigint>;
61+
// @ts-expect-error
62+
type invalid2 = ExcludeStrict<string, Uppercase<string>>;
63+
64+
// Optional and readonly modifiers
65+
expectType<never>({} as ExcludeStrict<{a: string; b: number}, {a?: string}>);
66+
expectType<{c: string; d: number}>({} as ExcludeStrict<{a: string; b: number} | {c: string; d: number}, {a?: string}>);
67+
expectType<never>({} as ExcludeStrict<string[], readonly string[]>);
68+
69+
// @ts-expect-error
70+
type invalid3 = ExcludeStrict<{a?: string; b: number}, {a: string}>;
71+
// @ts-expect-error
72+
type invalid4 = ExcludeStrict<readonly string[], string[]>;
73+
74+
// Index signatures
75+
expectType<{a: string; b: number}>(
76+
{} as ExcludeStrict<{a: string; b: number} | {c: true; d: false}, Record<string, boolean>>,
77+
);
78+
79+
// @ts-expect-error
80+
type invalid5 = ExcludeStrict<{a: string; b: number} | {c: true; d: false}, Record<string, string>>;
81+
82+
// `any` and `never`
83+
expectType<never>(
84+
{} as ExcludeStrict<string | {a: string; b: number} | string[], any>,
85+
);
86+
expectType<string | {a: string; b: number} | string[]>(
87+
{} as ExcludeStrict<string | {a: string; b: number} | string[], never>,
88+
);
89+
90+
// Miscellaneous
91+
expectType<{x: number; y: number}>({} as ExcludeStrict<[number, number] | {x: number; y: number}, unknown[]>);
92+
expectType<[number, number, number]>({} as ExcludeStrict<[number, number] | [number, number, number], {length: 2}>);
93+
expectType<string | string[]>({} as ExcludeStrict<string | string[] | {data: string | string[]}, {data: unknown}>);

0 commit comments

Comments
 (0)