Skip to content

Commit 98d24fa

Browse files
nigeltroysom-sm
andauthored
Add ExtractStrict type (#1119)
Co-authored-by: Som Shekhar Mukherjee <[email protected]>
1 parent c5bdea9 commit 98d24fa

File tree

4 files changed

+153
-0
lines changed

4 files changed

+153
-0
lines changed

index.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,6 @@ export type {LastArrayElement} from './source/last-array-element';
176176
export type {GlobalThis} from './source/global-this';
177177
export type {PackageJson} from './source/package-json';
178178
export type {TsConfigJson} from './source/tsconfig-json';
179+
180+
// Improved built-in
181+
export type {ExtractStrict} from './source/extract-strict';

readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,10 @@ type ShouldBeNever = IfAny<'not any', 'not never', 'never'>;
333333
- [`PackageJson`](source/package-json.d.ts) - Type for [npm's `package.json` file](https://docs.npmjs.com/creating-a-package-json-file). It also includes support for [TypeScript Declaration Files](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html).
334334
- [`TsConfigJson`](source/tsconfig-json.d.ts) - Type for [TypeScript's `tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html).
335335

336+
### Improved built-in
337+
338+
- [`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`.
339+
336340
## Declined types
337341

338342
*If we decline a type addition, we will make sure to document the better solution here.*

source/extract-strict.d.ts

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

test-d/extract-strict.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {expectType} from 'tsd';
2+
import type {ExtractStrict} from '../source/extract-strict';
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 largeShirtSizes: ExtractStrict<ShirtSize, LargeShirtSize>;
11+
expectType<LargeShirtSize>(largeShirtSizes);
12+
13+
declare const smallShirtSizes: ExtractStrict<ShirtSize, SmallShirtSize>;
14+
expectType<SmallShirtSize>(smallShirtSizes);
15+
16+
// @ts-expect-error
17+
declare const allInvalidShirtSizes: ExtractStrict<ShirtSize, 'skyscraper-large' | 'atom-small'>;
18+
19+
// @ts-expect-error
20+
declare const someInvalidShirtSizes: ExtractStrict<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+
declare const foobarByA: ExtractStrict<Foobar, {a: string}>;
40+
expectType<Foobar>(foobarByA);
41+
42+
declare const onlyFooByKind: ExtractStrict<Foobar, {kind: 'foo'}>;
43+
expectType<Foo>(onlyFooByKind);
44+
45+
declare const onlyFooByB: ExtractStrict<Foobar, {b: string}>;
46+
expectType<Foo>(onlyFooByB);
47+
48+
declare const onlyBarByC: ExtractStrict<Foobar, {c: boolean}>;
49+
expectType<Bar>(onlyBarByC);
50+
51+
declare const foobarByUnionBC: ExtractStrict<Foobar, {b: string} | {c: boolean}>;
52+
expectType<Foobar>(foobarByUnionBC);
53+
54+
// @ts-expect-error
55+
declare const invalidLoneField: ExtractStrict<Foobar, {d: string}>;
56+
57+
// @ts-expect-error
58+
declare const invalidMixedFields: ExtractStrict<Foobar, {kind: 'foo'; d: string}>;
59+
60+
// @ts-expect-error
61+
declare const undefinedField: ExtractStrict<Foobar, undefined>;
62+
63+
// Primitives
64+
expectType<number>({} as ExtractStrict<string | number, number>);
65+
expectType<number | bigint>({} as ExtractStrict<string | number | bigint, number | bigint>);
66+
expectType<'bar' | 'baz'>({} as ExtractStrict<'foo' | 'bar' | 'baz', `b${string}`>);
67+
68+
// @ts-expect-error
69+
type invalid1 = ExtractStrict<string | number | boolean, number | bigint>;
70+
// @ts-expect-error
71+
type invalid2 = ExtractStrict<string, Uppercase<string>>;
72+
73+
// Optional and readonly modifiers
74+
expectType<{a: string; b: number}>({} as ExtractStrict<{a: string; b: number}, {a?: string}>);
75+
expectType<string[]>({} as ExtractStrict<string[], readonly string[]>);
76+
77+
// @ts-expect-error
78+
type invalid3 = ExtractStrict<{a?: string; b: number}, {a: string}>;
79+
// @ts-expect-error
80+
type invalid4 = ExtractStrict<readonly string[], string[]>;
81+
82+
// Index signatures
83+
expectType<{c: true; d: false}>(
84+
{} as ExtractStrict<{a: string; b: number} | {c: true; d: false}, Record<string, boolean>>,
85+
);
86+
87+
// @ts-expect-error
88+
type invalid5 = ExtractStrict<{a: string; b: number} | {c: true; d: false}, Record<string, string>>;
89+
90+
// `any` and `never`
91+
expectType<string | {a: string; b: number} | string[]>(
92+
{} as ExtractStrict<string | {a: string; b: number} | string[], any>,
93+
);
94+
expectType<never>(
95+
{} as ExtractStrict<string | {a: string; b: number} | string[], never>,
96+
);
97+
98+
// Miscellaneous
99+
expectType<[number, number]>({} as ExtractStrict<[number, number] | {x: number; y: number}, unknown[]>);
100+
expectType<[number, number]>({} as ExtractStrict<[number, number] | [number, number, number], {length: 2}>);
101+
expectType<{data: string | string[]}>({} as ExtractStrict<string | string[] | {data: string | string[]}, {data: unknown}>);

0 commit comments

Comments
 (0)