Skip to content

Commit 7be3f99

Browse files
authored
Better typing for throws assertions
Fixes #1893.
1 parent ff09749 commit 7be3f99

File tree

7 files changed

+152
-31
lines changed

7 files changed

+152
-31
lines changed

docs/recipes/flow.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,37 @@ test('an actual test', t => {
6868
```
6969

7070
Note that, despite the type cast above, when executing `t.context` is an empty object unless it's assigned.
71+
72+
## Typing `throws` assertions
73+
74+
The `t.throws()` and `t.throwsAsync()` assertions are typed to always return an Error. You can customize the error class using generics:
75+
76+
```js
77+
// @flow
78+
import test from 'ava';
79+
80+
class CustomError extends Error {
81+
parent: Error;
82+
83+
constructor(parent) {
84+
super(parent.message);
85+
this.parent = parent;
86+
}
87+
}
88+
89+
function myFunc() {
90+
throw new CustomError(TypeError('🙈'));
91+
};
92+
93+
test('throws', t => {
94+
const err = t.throws<CustomError>(myFunc);
95+
t.is(err.parent.name, 'TypeError');
96+
});
97+
98+
test('throwsAsync', async t => {
99+
const err = await t.throwsAsync<CustomError>(async () => myFunc());
100+
t.is(err.parent.name, 'TypeError');
101+
});
102+
```
103+
104+
Note that, despite the typing, the assertion returns `undefined` if it fails. Typing the assertions as returning `Error | undefined` didn't seem like the pragmatic choice.

docs/recipes/typescript.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,36 @@ test('foo is bar', macro, 'bar');
135135
```
136136

137137
Note that, despite the type cast above, when executing `t.context` is an empty object unless it's assigned.
138+
139+
## Typing `throws` assertions
140+
141+
The `t.throws()` and `t.throwsAsync()` assertions are typed to always return an Error. You can customize the error class using generics:
142+
143+
```ts
144+
import test from 'ava';
145+
146+
class CustomError extends Error {
147+
parent: Error
148+
149+
constructor(parent) {
150+
super(parent.message);
151+
this.parent = parent;
152+
}
153+
}
154+
155+
function myFunc() {
156+
throw new CustomError(TypeError('🙈'));
157+
};
158+
159+
test('throws', t => {
160+
const err = t.throws<CustomError>(myFunc);
161+
t.is(err.parent.name, 'TypeError');
162+
});
163+
164+
test('throwsAsync', async t => {
165+
const err = await t.throwsAsync<CustomError>(async () => myFunc());
166+
t.is(err.parent.name, 'TypeError');
167+
});
168+
```
169+
170+
Note that, despite the typing, the assertion returns `undefined` if it fails. Typing the assertions as returning `Error | undefined` didn't seem like the pragmatic choice.

index.d.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -227,31 +227,31 @@ export interface ThrowsAssertion {
227227
/**
228228
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
229229
*/
230-
(fn: () => any, expectations?: null, message?: string): any;
230+
<ThrownError extends Error>(fn: () => any, expectations?: null, message?: string): ThrownError;
231231

232232
/**
233233
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
234234
* The error must be an instance of the given constructor.
235235
*/
236-
(fn: () => any, constructor: Constructor, message?: string): any;
236+
<ThrownError extends Error>(fn: () => any, constructor: Constructor, message?: string): ThrownError;
237237

238238
/**
239239
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
240240
* The error must have a message that matches the regular expression.
241241
*/
242-
(fn: () => any, regex: RegExp, message?: string): any;
242+
<ThrownError extends Error>(fn: () => any, regex: RegExp, message?: string): ThrownError;
243243

244244
/**
245245
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
246246
* The error must have a message equal to `errorMessage`.
247247
*/
248-
(fn: () => any, errorMessage: string, message?: string): any;
248+
<ThrownError extends Error>(fn: () => any, errorMessage: string, message?: string): ThrownError;
249249

250250
/**
251251
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
252252
* The error must satisfy all expectations.
253253
*/
254-
(fn: () => any, expectations: ThrowsExpectation, message?: string): any;
254+
<ThrownError extends Error>(fn: () => any, expectations: ThrowsExpectation, message?: string): ThrownError;
255255

256256
/** Skip this assertion. */
257257
skip(fn: () => any, expectations?: any, message?: string): void;
@@ -262,61 +262,61 @@ export interface ThrowsAsyncAssertion {
262262
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
263263
* value. You must await the result.
264264
*/
265-
(fn: () => PromiseLike<any>, expectations?: null, message?: string): Promise<any>;
265+
<ThrownError extends Error>(fn: () => PromiseLike<any>, expectations?: null, message?: string): Promise<ThrownError>;
266266

267267
/**
268268
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
269269
* value. You must await the result. The error must be an instance of the given constructor.
270270
*/
271-
(fn: () => PromiseLike<any>, constructor: Constructor, message?: string): Promise<any>;
271+
<ThrownError extends Error>(fn: () => PromiseLike<any>, constructor: Constructor, message?: string): Promise<ThrownError>;
272272

273273
/**
274274
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
275275
* value. You must await the result. The error must have a message that matches the regular expression.
276276
*/
277-
(fn: () => PromiseLike<any>, regex: RegExp, message?: string): Promise<any>;
277+
<ThrownError extends Error>(fn: () => PromiseLike<any>, regex: RegExp, message?: string): Promise<ThrownError>;
278278

279279
/**
280280
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
281281
* value. You must await the result. The error must have a message equal to `errorMessage`.
282282
*/
283-
(fn: () => PromiseLike<any>, errorMessage: string, message?: string): Promise<any>;
283+
<ThrownError extends Error>(fn: () => PromiseLike<any>, errorMessage: string, message?: string): Promise<ThrownError>;
284284

285285
/**
286286
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
287287
* value. You must await the result. The error must satisfy all expectations.
288288
*/
289-
(fn: () => PromiseLike<any>, expectations: ThrowsExpectation, message?: string): Promise<any>;
289+
<ThrownError extends Error>(fn: () => PromiseLike<any>, expectations: ThrowsExpectation, message?: string): Promise<ThrownError>;
290290

291291
/**
292292
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
293293
* rejection reason. You must await the result.
294294
*/
295-
(promise: PromiseLike<any>, expectations?: null, message?: string): Promise<any>;
295+
<ThrownError extends Error>(promise: PromiseLike<any>, expectations?: null, message?: string): Promise<ThrownError>;
296296

297297
/**
298298
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
299299
* rejection reason. You must await the result. The error must be an instance of the given constructor.
300300
*/
301-
(promise: PromiseLike<any>, constructor: Constructor, message?: string): Promise<any>;
301+
<ThrownError extends Error>(promise: PromiseLike<any>, constructor: Constructor, message?: string): Promise<ThrownError>;
302302

303303
/**
304304
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
305305
* rejection reason. You must await the result. The error must have a message that matches the regular expression.
306306
*/
307-
(promise: PromiseLike<any>, regex: RegExp, message?: string): Promise<any>;
307+
<ThrownError extends Error>(promise: PromiseLike<any>, regex: RegExp, message?: string): Promise<ThrownError>;
308308

309309
/**
310310
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
311311
* rejection reason. You must await the result. The error must have a message equal to `errorMessage`.
312312
*/
313-
(promise: PromiseLike<any>, errorMessage: string, message?: string): Promise<any>;
313+
<ThrownError extends Error>(promise: PromiseLike<any>, errorMessage: string, message?: string): Promise<ThrownError>;
314314

315315
/**
316316
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
317317
* rejection reason. You must await the result. The error must satisfy all expectations.
318318
*/
319-
(promise: PromiseLike<any>, expectations: ThrowsExpectation, message?: string): Promise<any>;
319+
<ThrownError extends Error>(promise: PromiseLike<any>, expectations: ThrowsExpectation, message?: string): Promise<ThrownError>;
320320

321321
/** Skip this assertion. */
322322
skip(thrower: any, expectations?: any, message?: string): void;

index.js.flow

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -240,31 +240,31 @@ export interface ThrowsAssertion {
240240
/**
241241
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
242242
*/
243-
(fn: () => any, expectations?: null, message?: string): any;
243+
<ThrownError: Error>(fn: () => any, expectations?: null, message?: string): ThrownError;
244244

245245
/**
246246
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
247247
* The error must be an instance of the given constructor.
248248
*/
249-
(fn: () => any, constructor: Constructor, message?: string): any;
249+
<ThrownError: Error>(fn: () => any, constructor: Constructor, message?: string): ThrownError;
250250

251251
/**
252252
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
253253
* The error must have a message that matches the regular expression.
254254
*/
255-
(fn: () => any, regex: RegExp, message?: string): any;
255+
<ThrownError: Error>(fn: () => any, regex: RegExp, message?: string): ThrownError;
256256

257257
/**
258258
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
259259
* The error must have a message equal to `errorMessage`.
260260
*/
261-
(fn: () => any, errorMessage: string, message?: string): any;
261+
<ThrownError: Error>(fn: () => any, errorMessage: string, message?: string): ThrownError;
262262

263263
/**
264264
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
265265
* The error must satisfy all expectations.
266266
*/
267-
(fn: () => any, expectations: ThrowsExpectation, message?: string): any;
267+
<ThrownError: Error>(fn: () => any, expectations: ThrowsExpectation, message?: string): ThrownError;
268268

269269
/** Skip this assertion. */
270270
skip(fn: () => any, expectations?: any, message?: string): void;
@@ -275,61 +275,61 @@ export interface ThrowsAsyncAssertion {
275275
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
276276
* value. You must await the result.
277277
*/
278-
(fn: () => PromiseLike<any>, expectations?: null, message?: string): Promise<any>;
278+
<ThrownError: Error>(fn: () => PromiseLike<any>, expectations?: null, message?: string): Promise<ThrownError>;
279279

280280
/**
281281
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
282282
* value. You must await the result. The error must be an instance of the given constructor.
283283
*/
284-
(fn: () => PromiseLike<any>, constructor: Constructor, message?: string): Promise<any>;
284+
<ThrownError: Error>(fn: () => PromiseLike<any>, constructor: Constructor, message?: string): Promise<ThrownError>;
285285

286286
/**
287287
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
288288
* value. You must await the result. The error must have a message that matches the regular expression.
289289
*/
290-
(fn: () => PromiseLike<any>, regex: RegExp, message?: string): Promise<any>;
290+
<ThrownError: Error>(fn: () => PromiseLike<any>, regex: RegExp, message?: string): Promise<ThrownError>;
291291

292292
/**
293293
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
294294
* value. You must await the result. The error must have a message equal to `errorMessage`.
295295
*/
296-
(fn: () => PromiseLike<any>, errorMessage: string, message?: string): Promise<any>;
296+
<ThrownError: Error>(fn: () => PromiseLike<any>, errorMessage: string, message?: string): Promise<ThrownError>;
297297

298298
/**
299299
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
300300
* value. You must await the result. The error must satisfy all expectations.
301301
*/
302-
(fn: () => PromiseLike<any>, expectations: ThrowsExpectation, message?: string): Promise<any>;
302+
<ThrownError: Error>(fn: () => PromiseLike<any>, expectations: ThrowsExpectation, message?: string): Promise<ThrownError>;
303303

304304
/**
305305
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
306306
* rejection reason. You must await the result.
307307
*/
308-
(promise: PromiseLike<any>, expectations?: null, message?: string): Promise<any>;
308+
<ThrownError: Error>(promise: PromiseLike<any>, expectations?: null, message?: string): Promise<ThrownError>;
309309

310310
/**
311311
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
312312
* rejection reason. You must await the result. The error must be an instance of the given constructor.
313313
*/
314-
(promise: PromiseLike<any>, constructor: Constructor, message?: string): Promise<any>;
314+
<ThrownError: Error>(promise: PromiseLike<any>, constructor: Constructor, message?: string): Promise<ThrownError>;
315315

316316
/**
317317
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
318318
* rejection reason. You must await the result. The error must have a message that matches the regular expression.
319319
*/
320-
(promise: PromiseLike<any>, regex: RegExp, message?: string): Promise<any>;
320+
<ThrownError: Error>(promise: PromiseLike<any>, regex: RegExp, message?: string): Promise<ThrownError>;
321321

322322
/**
323323
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
324324
* rejection reason. You must await the result. The error must have a message equal to `errorMessage`.
325325
*/
326-
(promise: PromiseLike<any>, errorMessage: string, message?: string): Promise<any>;
326+
<ThrownError: Error>(promise: PromiseLike<any>, errorMessage: string, message?: string): Promise<ThrownError>;
327327

328328
/**
329329
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
330330
* rejection reason. You must await the result. The error must satisfy all expectations.
331331
*/
332-
(promise: PromiseLike<any>, expectations: ThrowsExpectation, message?: string): Promise<any>;
332+
<ThrownError: Error>(promise: PromiseLike<any>, expectations: ThrowsExpectation, message?: string): Promise<ThrownError>;
333333

334334
/** Skip this assertion. */
335335
skip(thrower: any, expectations?: any, message?: string): void;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@
172172
"media/**",
173173
"test/fixture/{source-map-initial,syntax-error}.js",
174174
"test/fixture/snapshots/test-sourcemaps/build/**",
175-
"**/*.ts"
175+
"**/*.ts",
176+
"test/flow-types/*"
176177
],
177178
"rules": {
178179
"no-use-extend-native/no-use-extend-native": "off",

test/flow-types/throws.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// @flow
2+
import test from '../../index.js.flow';
3+
4+
class CustomError extends Error {
5+
foo: string;
6+
7+
constructor() {
8+
super();
9+
this.foo = 'foo';
10+
}
11+
}
12+
13+
test('throws', t => {
14+
const err1: Error = t.throws(() => {});
15+
// t.is(err1.foo, 'foo');
16+
const err2: CustomError = t.throws(() => {});
17+
t.is(err2.foo, 'foo');
18+
const err3 = t.throws<CustomError>(() => {});
19+
t.is(err3.foo, 'foo');
20+
});
21+
22+
test('throwsAsync', async t => {
23+
const err1: Error = await t.throwsAsync(Promise.reject());
24+
// t.is(err1.foo, 'foo');
25+
const err2 = await t.throwsAsync<CustomError>(Promise.reject());
26+
t.is(err2.foo, 'foo');
27+
});

test/ts-types/throws.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import test from '../..';
2+
3+
class CustomError extends Error {
4+
foo: string;
5+
6+
constructor() {
7+
super();
8+
this.foo = 'foo';
9+
}
10+
}
11+
12+
test('throws', t => {
13+
const err1: Error = t.throws(() => {});
14+
// t.is(err1.foo, 'foo');
15+
const err2: CustomError = t.throws(() => {});
16+
t.is(err2.foo, 'foo');
17+
const err3 = t.throws<CustomError>(() => {});
18+
t.is(err3.foo, 'foo');
19+
});
20+
21+
test('throwsAsync', async t => {
22+
const err1: Error = await t.throwsAsync(Promise.reject());
23+
// t.is(err1.foo, 'foo');
24+
const err2 = await t.throwsAsync<CustomError>(Promise.reject());
25+
t.is(err2.foo, 'foo');
26+
});

0 commit comments

Comments
 (0)