diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..6c7b11a --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,135 @@ +import type { Boom } from "@hapi/boom"; + +/** + * Possible type matching rule. One of: + * - An error constructor (e.g. `SyntaxError`). + * - `'system'` - matches any language native error or node assertions. + * - `'boom'` - matches [**boom**](https://github.com/hapijs/boom) errors. + * - `'abort'` - matches an `AbortError`, as generated on an `AbortSignal` by `AbortController.abort()`. + * - `'timeout'` - matches a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. + * - an object where each property is compared with the error and must match the error property + * value. All the properties in the object must match the error but do not need to include all + * the error properties. + */ +type TypeRule = 'system' | 'boom' | 'abort' | 'timeout' | ErrorConstructor | { [key: PropertyKey]: any }; + +type Decoration = { [key: string]: any }; + +interface BounceOptions { + + /** + * An object which is assigned to the `err`, copying the properties onto the error. + */ + decorate?: { [key: string]: any }; + + /** + * An error used to override `err` when `err` matches. + * If used with `decorate`, the `override` object is modified. + */ + override?: Error; + + /** + * If `true`, the error is returned instead of thrown. Defaults to `false`. + */ + return?: boolean; +} + +/** + * Throws the error passed if it matches any of the specified rules. + * + * @param err - the error object to test. + * @param type - a single {@link TypeRule `TypeRule`} or an array of {@link TypeRule `TypeRule`}. + * @param options - optional {@link BounceOptions settings}. + * + * @returns possibly an `Error` depending on value of the `return` and `decorate` options. + */ +export function rethrow(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D, override: E }): (E & D) | undefined; +export function rethrow(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; +export function rethrow(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; +export function rethrow(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; +export function rethrow(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; + +/** + * The opposite action of {@link rethrow `rethrow()`}. Ignores any errors matching the specified `types`. + * Any error not matching is thrown after applying the `options`. + * + * @param err - the error object to test. + * @param type - a single {@link TypeRule `TypeRule`} or an array of {@link TypeRule `TypeRule`}. + * @param options - optional {@link BounceOptions settings}. + * + * @returns possibly an `Error` depending on value of the `return` and `decorate` options. + */ +export function ignore(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D, override: E }): (E & D) | undefined; +export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; +export function ignore(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; +export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; +export function ignore(err: any, types: 'boom', options: { return?: false | undefined }): asserts err is Boom; +export function ignore(err: any, types: 'boom'): asserts err is Boom; +export function ignore(err: any, types: TypeRule | TypeRule[], options: { return?: false | undefined }): asserts err is Error; +export function ignore(err: any, types: TypeRule | TypeRule[]): asserts err is Error; +export function ignore(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; + +type Action = 'rethrow' | 'ignore'; + +/** + * Awaits for the value to resolve in the background and then apply either the {@link rethrow `rethrow()`} or {@link rethrow `ignore()`} action. + * + * @param operation - a function, promise, or value that is `await`ed on inside a `try...catch` + * and any error thrown processed by the `action` rule. + * @param action - one of `'rethrow'` or`'ignore'`. Defaults to`'rethrow'`. + * @param types - same as the `types` argument passed to {@link rethrow `rethrow()`} or {@link rethrow `ignore()`}. Defaults to `'system'`. + * @param options - same as the {@link BounceOptions `options`} argument passed to {@link rethrow `rethrow()`} or {@link rethrow `ignore()`}. + */ +export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions & { return: true, override: E, decorate: D }): Promise<(E & D) | undefined>; +export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions & { return: true, override: E }): Promise; +export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions & { return: true }): Promise; +export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions): Promise; + +/** + * Returns `true` when `err` is a [**boom**](https://github.com/hapijs/boom) error. + * + * @param err - Object to test. + */ +export function isBoom(err: unknown): err is Boom; + +/** + * Returns `true` when `err` is an error. + * + * @param err - Object to test. + */ +export function isError(err: unknown): err is Error; + +/** + * Return `true` when `err` is one of: + * - `EvalError` + * - `RangeError` + * - `ReferenceError` + * - `SyntaxError` + * - `TypeError` + * - `URIError` + * - Node's `AssertionError` + * - Hoek's `AssertError` + * + * @param err - Object to test. + */ +export function isSystem(err: unknown): err is Error; + +/** + * Returns `true` when `err` is an `AbortError`, as generated by `AbortSignal.abort()`. + * + * Note that unlike other errors, `AbortError` cannot be considered a class in itself. + * The best way to create a custom `AbortError` is with `new DOMException(message, 'AbortError')`. + * + * @param err - Object to test. + */ +export function isAbort(err: unknown): err is Error & { name: 'AbortError' }; + +/** + * Returns `true` when `err` is a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. + * + * Note that unlike other errors, `TimeoutError` cannot be considered a class in itself. + * The best way to create a custom `TimeoutError` is with `new DOMException(message, 'TimeoutError')`. + * + * @param err - Object to test. + */ +export function isTimeout(err: unknown): err is Error & { name: 'TimeoutError' }; diff --git a/package.json b/package.json index 8e0b078..77de38f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "3.0.1", "repository": "git://github.com/hapijs/bounce", "main": "lib/index.js", + "types": "lib/index.d.ts", "files": [ "lib" ], @@ -18,10 +19,12 @@ "devDependencies": { "@hapi/code": "^9.0.0", "@hapi/eslint-plugin": "^7.0.0", - "@hapi/lab": "^26.0.0" + "@hapi/lab": "^26.0.0", + "@types/node": "^18.19.57", + "typescript": "~5.6.3" }, "scripts": { - "test": "lab -a @hapi/code -t 100 -L", + "test": "lab -a @hapi/code -t 100 -L -Y", "test-cov-html": "lab -a @hapi/code -r html -o coverage.html -L" }, "license": "BSD-3-Clause" diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..bfa0c0b --- /dev/null +++ b/test/index.ts @@ -0,0 +1,130 @@ +import * as Bounce from '..'; +import * as Boom from '@hapi/boom'; +import * as Lab from '@hapi/lab'; + +const { expect } = Lab.types; + +// rethrow() + +expect.type(Bounce.rethrow(new Error(), 'system')); +expect.type(Bounce.rethrow(123, 'boom')); +expect.type(Bounce.rethrow(new TypeError(), ['boom', RangeError, { prop: true }])); +expect.type(Bounce.rethrow(new TypeError(), 'boom', { return: true })); +expect.type(Bounce.rethrow(null, 'boom', { return: true, override: new RangeError() })); +expect.type<(TypeError & { prop: string }) | undefined>(Bounce.rethrow(new TypeError(), 'boom', { return: true, decorate: { prop: 'ok' } })); + +expect.error(Bounce.rethrow(new Error())); +expect.error(Bounce.rethrow(new Error(), 'unknown')); +expect.error(Bounce.rethrow(new Error(), 'boom', true)); +expect.error(Bounce.rethrow(new Error(), 'boom', { unknown: true })); +expect.error(Bounce.rethrow(new Error(), 'boom', { decorate: 123 })); +expect.error(Bounce.rethrow(new Error(), 'boom', { override: {} })); + +// ignore() + +expect.type(Bounce.ignore(new TypeError(), 'system')); +expect.type(Bounce.ignore(new Boom.Boom(), 'boom')); +expect.type(Bounce.ignore(new RangeError(), ['boom', RangeError, { prop: true }])); +expect.type(Bounce.ignore(new TypeError(), 'boom', { return: true })); +expect.type(Bounce.ignore(null, 'boom', { return: true, override: new RangeError() })); +expect.type<(TypeError & { prop: string }) | undefined>(Bounce.ignore(new TypeError(), 'boom', { return: true, decorate: { prop: 'ok' } })); + +// Narrows the error type + +{ + const error = new TypeError() as any; + Bounce.ignore(error, TypeError); + expect.type(error); +} + +{ + const error = new Boom.Boom() as any; + Bounce.ignore(error, 'boom'); + expect.type(error); +} + +expect.error(Bounce.ignore(new Error())); +expect.error(Bounce.ignore(new Error(), 'unknown')); +expect.error(Bounce.ignore(new Error(), 'boom', true)); +expect.error(Bounce.ignore(new Error(), 'boom', { unknown: true })); +expect.error(Bounce.ignore(new Error(), 'boom', { decorate: 123 })); +expect.error(Bounce.ignore(new Error(), 'boom', { override: {} })); + +// background() + +expect.type>(Bounce.background(async () => undefined, 'ignore', 'system', { decorate: { a: true } })); +expect.type>(Bounce.background(async () => undefined, 'rethrow', [RangeError], { return: true })); +expect.type>(Bounce.background(async () => undefined, undefined, undefined, { return: true, override: new TypeError() })); + +expect.error(Bounce.background()); +expect.error(Bounce.background(true, 'unknown')); + +// isBoom() + +expect.type(Bounce.isBoom(new Error())); +expect.type(Bounce.isBoom({})); +{ + const obj = {}; + if (Bounce.isBoom(obj)) { + expect.type(obj); // Narrows type + } +} + +expect.error(Bounce.isBoom()); + +// isError() + +expect.type(Bounce.isError(new Error())); +expect.type(Bounce.isError(true)); +{ + const obj = {}; + if (Bounce.isError(obj)) { + expect.type(obj); // Narrows type + } +} + +expect.error(Bounce.isError()); + +// isSystem() + +expect.type(Bounce.isSystem(new Error())); +{ + const err = new TypeError(); + if (Bounce.isSystem(err)) { + expect.type(err); // Does not narrow type + } +} +{ + const obj = {}; + if (Bounce.isSystem(obj)) { + expect.type(obj); // Narrows type + } +} + +expect.error(Bounce.isSystem()); + +// isAbort() + +expect.type(Bounce.isAbort(0)); +{ + const err = AbortSignal.abort().reason as any; + if (Bounce.isAbort(err)) { + expect.type(err); // Narrows type + expect.type<'AbortError'>(err.name); // Narrows name + } +} + +expect.error(Bounce.isAbort()); + +// isTimeout() + +expect.type(Bounce.isTimeout(0)); +{ + const err = AbortSignal.timeout(1).reason as any; + if (Bounce.isTimeout(err)) { + expect.type(err); // Narrows type + expect.type<'TimeoutError'>(err.name); // Narrows name + } +} + +expect.error(Bounce.isTimeout());