-
-
Notifications
You must be signed in to change notification settings - Fork 601
Add ExtractStrict
type
#1119
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
base: main
Are you sure you want to change the base?
Add ExtractStrict
type
#1119
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import type {Exact} from './exact'; | ||
|
||
/** | ||
Extract members of a union type `Type` based on the | ||
fields in the given union type `Union`, where each | ||
union member of `Union` is only allowed to be a subset | ||
of some union member of `Type`. | ||
Comment on lines
+5
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is not true; consider the following example: type T = ExtractStrict<{foo: 1; bar: 1} | {baz: 1}, {foo: 1}>; The above instantiation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the other hand, for something like: type T = ExtractStrict<string | number, 'foo'>;
// ^? type T = never
If I understand the ask correctly, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the detailed review!
You're right here in terms of the type, but I'm open to amending the language to whatever you think makes sense to most users. I was thinking a subset of fields, not of the type. Let
100%. This is a great test case for me to add and calls for a better There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO, it'll be difficult to clearly explain this using technical terms, so it'd be better if we keep the text simple and explain it with examples. Like: /**
A stricter version of {@link Extract} that ensures every member of `Union` can successfully extract something from `Type`.
For e.g., `StrictExtract<string | number | boolean, number | bigint>` will error because `bigint` cannot extract anything from `string | number | boolean`.
@example
```
// Valid Examples
type Example1 = ExtractStrict<{status: 'success'; data: string[]} | {status: 'error'; error: string}, {status: 'success'}>;
//=> {status: 'success'; data: string[]}
type Example2 = ExtractStrict<'xs' | 's' | 'm' | 'l' | 'xl', 'xs' | 's'>;
//=> 'xs' | 's'
type Example3 = ExtractStrict<{x: number; y: number} | [number, number], unknown[]>;
//=> [number, number]
```
@example
```
// Invalid Examples
type Example1 = ExtractStrict<'xs' | 's' | 'm' | 'l' | 'xl', 'xl' | 'xxl'>;
// ~~~~~~~~~~~~
//=> Error: Type "'xl' | 'xxl'" does not satisfy the constraint 'never'.
type Example2 = ExtractStrict<{x: number; y: number} | {x: string; y: string}, unknown[]>;
// ~~~~~~~~~
//=> Error: Type 'unknown[]' does not satisfy the constraint 'never'.
```
@category Improved Builtin
*/ |
||
|
||
Constraint: ∀ U ∈ Union, U ⊆ T, where T ∈ Type | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can remove if this overcomplicates things, but I found this mathematical notation pretty useful |
||
|
||
@example | ||
``` | ||
type Foo = { | ||
kind: 'foo'; | ||
a: string; | ||
b: string; | ||
}; | ||
|
||
type Bar = { | ||
kind: 'bar'; | ||
a: string; | ||
b: number; | ||
c: boolean; | ||
}; | ||
|
||
type Foobar = Foo | Bar; | ||
|
||
type FoobarByA = ExtractStrict<Foobar, {a: string}>; | ||
// => Foobar | ||
|
||
type OnlyFooByKind = ExtractStrict<Foobar, {kind: 'foo'}>; | ||
// => Foo | ||
|
||
type OnlyFooByB = ExtractStrict<Foobar, {b: string}>; | ||
// => Foo | ||
|
||
type OnlyBarByC = ExtractStrict<Foobar, {c: boolean}>; | ||
// => Bar | ||
|
||
type InvalidUnionForType = ExtractStrict<Foobar, {d: string}>; | ||
// => Error: | ||
// Types of property 'd' are incompatible. | ||
// Type 'string' is not assignable to type 'never'. | ||
``` | ||
@category Improved Builtin | ||
*/ | ||
export type ExtractStrict< | ||
Type, | ||
/** | ||
* Only allow keys that are in some union member | ||
* of `Type`. Thus, each union member of `Union` | ||
* is only allowed to be a subset of some union | ||
* member of `Type`. | ||
*/ | ||
Union extends Partial<Exact<Type, Union>>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, is type Line =
| {x1: number; y1: number; x2: number; y2: number}
| [[x1: number, y1: number], [x2: number, y2: number]]
| [x1: number, y1: number, x2: number, y2: number];
type LineArrayNotations = ExtractStrict<Line, unknown[]>; // Errors
// ^? type LineArrayNotations = [[x1: number, y1: number], [x2: number, y2: number]] | [x1: number, y1: number, x2: number, y2: number]; The above instantiation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO, the strictness be simplified to just ensure if type ExtractStrict<
Type,
Union extends Extract<Type, Union> extends never ? never : unknown,
> = Extract<Type, Union>;
type T1 = ExtractStrict<1 | 2 | 3 | 4, 1 | 2 | 3>;
// ^? type T1 = 1 | 2 | 3
// @ts-expect-error
type T2 = ExtractStrict<1 | 2 | 3 | 4, "one" | "two">;
// But something like this will also pass
type T3 = ExtractStrict<1 | 2 | 3 | 4, 1 | 2 | "three">;
// ^? type T3 = 1 | 2 Playground: https://tsplay.dev/wQDG1W There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or maybe we can ensure that every member of type ExtractStrict<
Type,
Union extends [Union] extends [
Union extends unknown ? (Extract<Type, Union> extends never ? never : Union) : never,
]
? unknown
: never,
> = Extract<Type, Union>;
type T1 = ExtractStrict<1 | 2 | 3 | 4, 1 | 2 | 3>;
// ^? type T1 = 1 | 2 | 3
// @ts-expect-error
type T2 = ExtractStrict<1 | 2 | 3 | 4, "one" | "two">;
// @ts-expect-error
type T3 = ExtractStrict<1 | 2 | 3 | 4, 1 | 2 | "three">; Playground: https://tsplay.dev/m0kXqN So, both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like your idea of additionally checking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nigeltroy I'd rather prefer the second approach that I suggested. |
||
> = Extract<Type, Union>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import {expectType} from 'tsd'; | ||
import type {ExtractStrict} from '../source/extract-strict'; | ||
|
||
// Primitive union tests | ||
|
||
type ShirtSize = 'xxxl' | 'xxl' | 'xl' | 'l' | 'm' | 's' | 'xs' | 'xxs'; | ||
type LargeShirtSize = 'xxxl' | 'xxl' | 'xl' | 'l'; | ||
type SmallShirtSize = 's' | 'xs' | 'xxs'; | ||
|
||
declare const largeShirtSizes: ExtractStrict<ShirtSize, LargeShirtSize>; | ||
expectType<LargeShirtSize>(largeShirtSizes); | ||
|
||
declare const smallShirtSizes: ExtractStrict<ShirtSize, SmallShirtSize>; | ||
expectType<SmallShirtSize>(smallShirtSizes); | ||
|
||
// @ts-expect-error | ||
declare const allInvalidShirtSizes: ExtractStrict<ShirtSize, 'skyscraper-large' | 'atom-small'>; | ||
expectType<never>(allInvalidShirtSizes); | ||
|
||
// @ts-expect-error | ||
declare const someInvalidShirtSizes: ExtractStrict<ShirtSize, 'm' | 'atom-small'>; | ||
expectType<'m'>(someInvalidShirtSizes); // This is how native `Extract` works with primitives | ||
|
||
// Object union tests | ||
|
||
type Foo = { | ||
kind: 'foo'; | ||
a: string; | ||
b: string; | ||
}; | ||
|
||
type Bar = { | ||
kind: 'bar'; | ||
a: string; | ||
b: number; | ||
c: boolean; | ||
}; | ||
|
||
type Foobar = Foo | Bar; | ||
|
||
declare const foobarByA: ExtractStrict<Foobar, {a: string}>; | ||
expectType<Foobar>(foobarByA); | ||
|
||
declare const onlyFooByKind: ExtractStrict<Foobar, {kind: 'foo'}>; | ||
expectType<Foo>(onlyFooByKind); | ||
|
||
declare const onlyFooByB: ExtractStrict<Foobar, {b: string}>; | ||
expectType<Foo>(onlyFooByB); | ||
|
||
declare const onlyBarByC: ExtractStrict<Foobar, {c: boolean}>; | ||
expectType<Bar>(onlyBarByC); | ||
|
||
declare const foobarByUnionBC: ExtractStrict<Foobar, {b: string} | {c: boolean}>; | ||
expectType<Foobar>(foobarByUnionBC); | ||
|
||
// @ts-expect-error | ||
declare const invalidLoneField: ExtractStrict<Foobar, {d: string}>; | ||
expectType<never>(invalidLoneField); | ||
|
||
// @ts-expect-error | ||
declare const invalidMixedFields: ExtractStrict<Foobar, {kind: 'foo'; d: string}>; | ||
expectType<never>(invalidMixedFields); | ||
|
||
// @ts-expect-error | ||
declare const undefinedField: ExtractStrict<Foobar, undefined>; | ||
expectType<never>(undefinedField); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per this comment: #291 (comment), I have added this new section/category.