Skip to content

Commit 800bec3

Browse files
committed
feat:add ExtractStrict improved built-in
1 parent afd809a commit 800bec3

File tree

4 files changed

+129
-0
lines changed

4 files changed

+129
-0
lines changed

index.d.ts

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

readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ type ShouldBeNever = IfAny<'not any', 'not never', 'never'>;
332332
- [`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).
333333
- [`TsConfigJson`](source/tsconfig-json.d.ts) - Type for [TypeScript's `tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html).
334334

335+
### Improved Built-in
336+
337+
- [`ExtractStrict`](source/extract-strict.d.ts) - Like `Extract<Type, Union>`, but all members of `Union` are restricted to be subsets of some member of `Type`.
338+
335339
## Declined types
336340

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

source/extract-strict.d.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type {Exact} from './exact';
2+
3+
/**
4+
Extract members of a union type `Type` based on the
5+
fields in the given union type `Union`, where each
6+
union member of `Union` is only allowed to be a subset
7+
of some union member of `Type`.
8+
9+
Constraint: ∀ U ∈ Union, U ⊆ T, where T ∈ Type
10+
11+
@example
12+
```
13+
type Foo = {
14+
kind: 'foo';
15+
a: string;
16+
b: string;
17+
};
18+
19+
type Bar = {
20+
kind: 'bar';
21+
a: string;
22+
b: number;
23+
c: boolean;
24+
};
25+
26+
type Foobar = Foo | Bar;
27+
28+
type FoobarByA = ExtractStrict<Foobar, {a: string}>;
29+
// => Foobar
30+
31+
type OnlyFooByKind = ExtractStrict<Foobar, {kind: 'foo'}>;
32+
// => Foo
33+
34+
type OnlyFooByB = ExtractStrict<Foobar, {b: string}>;
35+
// => Foo
36+
37+
type OnlyBarByC = ExtractStrict<Foobar, {c: boolean}>;
38+
// => Bar
39+
40+
type InvalidUnionForType = ExtractStrict<Foobar, {d: string}>;
41+
// => Error:
42+
// Types of property 'd' are incompatible.
43+
// Type 'string' is not assignable to type 'never'.
44+
```
45+
@category Improved Builtin
46+
*/
47+
export type ExtractStrict<
48+
Type,
49+
/**
50+
* Only allow keys that are in some union member
51+
* of `Type`. Thus, each union member of `Union`
52+
* is only allowed to be a subset of some union
53+
* member of `Type`.
54+
*/
55+
Union extends Partial<Exact<Type, Union>>,
56+
> = Extract<Type, Union>;

test-d/extract-strict.ts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
expectType<never>(allInvalidShirtSizes);
19+
20+
// @ts-expect-error
21+
declare const someInvalidShirtSizes: ExtractStrict<ShirtSize, 'm' | 'atom-small'>;
22+
expectType<'m'>(someInvalidShirtSizes); // This is how native `Extract` works with primitives
23+
24+
// Object union tests
25+
26+
type Foo = {
27+
kind: 'foo';
28+
a: string;
29+
b: string;
30+
};
31+
32+
type Bar = {
33+
kind: 'bar';
34+
a: string;
35+
b: number;
36+
c: boolean;
37+
};
38+
39+
type Foobar = Foo | Bar;
40+
41+
declare const foobarByA: ExtractStrict<Foobar, {a: string}>;
42+
expectType<Foobar>(foobarByA);
43+
44+
declare const onlyFooByKind: ExtractStrict<Foobar, {kind: 'foo'}>;
45+
expectType<Foo>(onlyFooByKind);
46+
47+
declare const onlyFooByB: ExtractStrict<Foobar, {b: string}>;
48+
expectType<Foo>(onlyFooByB);
49+
50+
declare const onlyBarByC: ExtractStrict<Foobar, {c: boolean}>;
51+
expectType<Bar>(onlyBarByC);
52+
53+
declare const foobarByUnionBC: ExtractStrict<Foobar, {b: string} | {c: boolean}>;
54+
expectType<Foobar>(foobarByUnionBC);
55+
56+
// @ts-expect-error
57+
declare const invalidLoneField: ExtractStrict<Foobar, {d: string}>;
58+
expectType<never>(invalidLoneField);
59+
60+
// @ts-expect-error
61+
declare const invalidMixedFields: ExtractStrict<Foobar, {kind: 'foo'; d: string}>;
62+
expectType<never>(invalidMixedFields);
63+
64+
// @ts-expect-error
65+
declare const undefinedField: ExtractStrict<Foobar, undefined>;
66+
expectType<never>(undefinedField);

0 commit comments

Comments
 (0)