Skip to content

Better TypeScript typing for throws assertions #1956

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

Merged
merged 6 commits into from
Oct 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also include the contents of myFunc somewhere as it's not that clear where TypeError comes from.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

});

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 @@ -171,7 +171,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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a type exception. I think maybe there's a way to indicate that to tsc?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the // @ts-ignore comment to ignore the below line.

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');
});