Skip to content

Commit

Permalink
Better typing for throws assertions
Browse files Browse the repository at this point in the history
Fixes avajs#1893.
  • Loading branch information
novemberborn authored Oct 14, 2018
1 parent ff09749 commit 7be3f99
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 31 deletions.
34 changes: 34 additions & 0 deletions docs/recipes/flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,37 @@ test('an actual test', t => {
```

Note that, despite the type cast above, when executing `t.context` is an empty object unless it's assigned.

## Typing `throws` assertions

The `t.throws()` and `t.throwsAsync()` assertions are typed to always return an Error. You can customize the error class using generics:

```js
// @flow
import test from 'ava';

class CustomError extends Error {
parent: Error;

constructor(parent) {
super(parent.message);
this.parent = parent;
}
}

function myFunc() {
throw new CustomError(TypeError('🙈'));
};

test('throws', t => {
const err = t.throws<CustomError>(myFunc);
t.is(err.parent.name, 'TypeError');
});

test('throwsAsync', async t => {
const err = await t.throwsAsync<CustomError>(async () => myFunc());
t.is(err.parent.name, 'TypeError');
});
```

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.
33 changes: 33 additions & 0 deletions docs/recipes/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,36 @@ test('foo is bar', macro, 'bar');
```

Note that, despite the type cast above, when executing `t.context` is an empty object unless it's assigned.

## Typing `throws` assertions

The `t.throws()` and `t.throwsAsync()` assertions are typed to always return an Error. You can customize the error class using generics:

```ts
import test from 'ava';

class CustomError extends Error {
parent: Error

constructor(parent) {
super(parent.message);
this.parent = parent;
}
}

function myFunc() {
throw new CustomError(TypeError('🙈'));
};

test('throws', t => {
const err = t.throws<CustomError>(myFunc);
t.is(err.parent.name, 'TypeError');
});

test('throwsAsync', async t => {
const err = await t.throwsAsync<CustomError>(async () => myFunc());
t.is(err.parent.name, 'TypeError');
});
```

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.
30 changes: 15 additions & 15 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,31 +227,31 @@ export interface ThrowsAssertion {
/**
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
*/
(fn: () => any, expectations?: null, message?: string): any;
<ThrownError extends Error>(fn: () => any, expectations?: null, message?: string): ThrownError;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/** Skip this assertion. */
skip(thrower: any, expectations?: any, message?: string): void;
Expand Down
30 changes: 15 additions & 15 deletions index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -240,31 +240,31 @@ export interface ThrowsAssertion {
/**
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
*/
(fn: () => any, expectations?: null, message?: string): any;
<ThrownError: Error>(fn: () => any, expectations?: null, message?: string): ThrownError;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/** Skip this assertion. */
skip(thrower: any, expectations?: any, message?: string): void;
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@
"media/**",
"test/fixture/{source-map-initial,syntax-error}.js",
"test/fixture/snapshots/test-sourcemaps/build/**",
"**/*.ts"
"**/*.ts",
"test/flow-types/*"
],
"rules": {
"no-use-extend-native/no-use-extend-native": "off",
Expand Down
27 changes: 27 additions & 0 deletions test/flow-types/throws.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @flow
import test from '../../index.js.flow';

class CustomError extends Error {
foo: string;

constructor() {
super();
this.foo = 'foo';
}
}

test('throws', t => {
const err1: Error = t.throws(() => {});
// t.is(err1.foo, 'foo');
const err2: CustomError = t.throws(() => {});
t.is(err2.foo, 'foo');
const err3 = t.throws<CustomError>(() => {});
t.is(err3.foo, 'foo');
});

test('throwsAsync', async t => {
const err1: Error = await t.throwsAsync(Promise.reject());
// t.is(err1.foo, 'foo');
const err2 = await t.throwsAsync<CustomError>(Promise.reject());
t.is(err2.foo, 'foo');
});
26 changes: 26 additions & 0 deletions test/ts-types/throws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import test from '../..';

class CustomError extends Error {
foo: string;

constructor() {
super();
this.foo = 'foo';
}
}

test('throws', t => {
const err1: Error = t.throws(() => {});
// t.is(err1.foo, 'foo');
const err2: CustomError = t.throws(() => {});
t.is(err2.foo, 'foo');
const err3 = t.throws<CustomError>(() => {});
t.is(err3.foo, 'foo');
});

test('throwsAsync', async t => {
const err1: Error = await t.throwsAsync(Promise.reject());
// t.is(err1.foo, 'foo');
const err2 = await t.throwsAsync<CustomError>(Promise.reject());
t.is(err2.foo, 'foo');
});

0 comments on commit 7be3f99

Please sign in to comment.