Skip to content

Commit 7827d7d

Browse files
authored
Add .exclude and .extract helpers (#220)
* Add `.excluding` and `.extracting` helpers * chore: change files * Make naming less weird * chore: change files * Add more test cases Co-authored-by: mmkal <mmkal@users.noreply.github.com>
1 parent c7931e6 commit 7827d7d

File tree

4 files changed

+87
-0
lines changed

4 files changed

+87
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "Add `.exclude` and `.extract` helpers (#220)",
5+
"type": "minor",
6+
"packageName": "expect-type"
7+
}
8+
],
9+
"packageName": "expect-type",
10+
"email": "mmkal@users.noreply.github.com"
11+
}

packages/expect-type/readme.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,43 @@ expectTypeOf(1).not.toBeUndefined()
179179
expectTypeOf(1).not.toBeNullable()
180180
```
181181

182+
Use `.extract` and `.exclude` to narrow down complex union types:
183+
184+
```typescript
185+
type ResponsiveProp<T> = T | T[] | {xs?: T; sm?: T; md?: T}
186+
const getResponsiveProp = <T>(props: T): ResponsiveProp<T> => ({})
187+
type CSSProperties = {margin?: string; padding?: string}
188+
189+
const cssProperties: CSSProperties = {margin: '1px', padding: '2px'}
190+
191+
expectTypeOf(getResponsiveProp(cssProperties))
192+
.exclude<unknown[]>()
193+
.exclude<{xs?: unknown}>()
194+
.toEqualTypeOf<CSSProperties>()
195+
196+
expectTypeOf(getResponsiveProp(cssProperties))
197+
.extract<unknown[]>()
198+
.toEqualTypeOf<CSSProperties[]>()
199+
200+
expectTypeOf(getResponsiveProp(cssProperties))
201+
.extract<{xs?: any}>()
202+
.toEqualTypeOf<{xs?: CSSProperties; sm?: CSSProperties; md?: CSSProperties}>()
203+
204+
expectTypeOf<ResponsiveProp<number>>().exclude<number | number[]>().toHaveProperty('sm')
205+
expectTypeOf<ResponsiveProp<number>>().exclude<number | number[]>().not.toHaveProperty('xxl')
206+
```
207+
208+
`.extract` and `.exclude` return never if no types remain after exclusion:
209+
210+
```typescript
211+
type Person = {name: string; age: number}
212+
type Customer = Person & {customerId: string}
213+
type Employee = Person & {employeeId: string}
214+
215+
expectTypeOf<Customer | Employee>().extract<{foo: string}>().toBeNever()
216+
expectTypeOf<Customer | Employee>().exclude<{name: string}>().toBeNever()
217+
```
218+
182219
Make assertions about object properties:
183220

184221
```typescript

packages/expect-type/src/__tests__/index.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable @typescript-eslint/no-empty-function */
22
import {expectTypeOf} from '..'
33

4+
/* eslint prettier/prettier: ["warn", { "singleQuote": true, "semi": false, "arrowParens": "avoid", "trailingComma": "es5", "bracketSpacing": false, "endOfLine": "auto", "printWidth": 100 }] */
5+
46
test("Check an object's type with `.toEqualTypeOf`", () => {
57
expectTypeOf({a: 1}).toEqualTypeOf<{a: number}>()
68
})
@@ -98,6 +100,39 @@ test('More `.not` examples', () => {
98100
expectTypeOf(1).not.toBeNullable()
99101
})
100102

103+
test('Use `.extract` and `.exclude` to narrow down complex union types', () => {
104+
type ResponsiveProp<T> = T | T[] | {xs?: T; sm?: T; md?: T}
105+
const getResponsiveProp = <T>(props: T): ResponsiveProp<T> => ({})
106+
type CSSProperties = {margin?: string; padding?: string}
107+
108+
const cssProperties: CSSProperties = {margin: '1px', padding: '2px'}
109+
110+
expectTypeOf(getResponsiveProp(cssProperties))
111+
.exclude<unknown[]>()
112+
.exclude<{xs?: unknown}>()
113+
.toEqualTypeOf<CSSProperties>()
114+
115+
expectTypeOf(getResponsiveProp(cssProperties))
116+
.extract<unknown[]>()
117+
.toEqualTypeOf<CSSProperties[]>()
118+
119+
expectTypeOf(getResponsiveProp(cssProperties))
120+
.extract<{xs?: any}>()
121+
.toEqualTypeOf<{xs?: CSSProperties; sm?: CSSProperties; md?: CSSProperties}>()
122+
123+
expectTypeOf<ResponsiveProp<number>>().exclude<number | number[]>().toHaveProperty('sm')
124+
expectTypeOf<ResponsiveProp<number>>().exclude<number | number[]>().not.toHaveProperty('xxl')
125+
})
126+
127+
test('`.extract` and `.exclude` return never if no types remain after exclusion', () => {
128+
type Person = {name: string; age: number}
129+
type Customer = Person & {customerId: string}
130+
type Employee = Person & {employeeId: string}
131+
132+
expectTypeOf<Customer | Employee>().extract<{foo: string}>().toBeNever()
133+
expectTypeOf<Customer | Employee>().exclude<{name: string}>().toBeNever()
134+
})
135+
101136
test('Make assertions about object properties', () => {
102137
const obj = {a: 1, b: ''}
103138

packages/expect-type/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ export interface ExpectTypeOf<Actual, B extends boolean> {
104104
key: K,
105105
...MISMATCH: MismatchArgs<Extends<K, keyof Actual>, B>
106106
) => K extends keyof Actual ? ExpectTypeOf<Actual[K], B> : true
107+
extract: <V>(v?: V) => ExpectTypeOf<Extract<Actual, V>, B>
108+
exclude: <V>(v?: V) => ExpectTypeOf<Exclude<Actual, V>, B>
107109
parameter: <K extends keyof Params<Actual>>(number: K) => ExpectTypeOf<Params<Actual>[K], B>
108110
parameters: ExpectTypeOf<Params<Actual>, B>
109111
constructorParameters: ExpectTypeOf<ConstructorParams<Actual>, B>
@@ -174,6 +176,8 @@ export const expectTypeOf: _ExpectTypeOf = <Actual>(actual?: Actual): ExpectType
174176
toEqualTypeOf: fn,
175177
toBeCallableWith: fn,
176178
toBeConstructibleWith: fn,
179+
extract: expectTypeOf,
180+
exclude: expectTypeOf,
177181
toHaveProperty: expectTypeOf,
178182
parameter: expectTypeOf,
179183
}

0 commit comments

Comments
 (0)