diff --git a/lib/src/helpers/customError.ts b/lib/src/helpers/customError.ts index 745204e4..9e710c29 100644 --- a/lib/src/helpers/customError.ts +++ b/lib/src/helpers/customError.ts @@ -53,8 +53,12 @@ function _setName(baseClass: any, name: string) { * @group Error * @param name - The name of the Custom Error * @param constructCb - [Optional] An optional callback function to call when a - * new Customer Error instance is being created. + * new Custom Error instance is being created. * @param errorBase - [Optional] (since v0.9.6) The error class to extend for this class, defaults to Error. + * @param superArgsFn - [Optional] (since v0.12.7) An optional function that receives the constructor arguments and + * returns the arguments to pass to the base class constructor. When not provided all constructor + * arguments are forwarded to the base class. Use this to support a different argument order or + * to pass a subset of arguments to the base class (similar to calling `super(...)` in a class). * @returns A new Error `class` * @example * ```ts @@ -115,13 +119,41 @@ function _setName(baseClass: any, name: string) { * theStartupError instanceof Error; // true * theStartupError instanceof AppError; // true * theStartupError instanceof StartupError; // true + * + * // ---------------------------------------------------------- + * // Custom error with reordered / transformed arguments + * // (the superArgsFn maps constructor args to base class args) + * // ---------------------------------------------------------- + * + * interface HttpErrorConstructor extends CustomErrorConstructor { + * new(statusCode: number, message: string): HttpError; + * (statusCode: number, message: string): HttpError; + * } + * + * interface HttpError extends Error { + * readonly statusCode: number; + * } + * + * // HttpError takes (statusCode, message) but base Error expects (message) + * let MyHttpError = createCustomError("HttpError", + * (self, args) => { + * self.statusCode = args[0]; + * }, + * Error, + * (args) => [ args[1] ] // pass only the message to base Error constructor + * ); + * + * let err = new MyHttpError(404, "Not Found"); + * err.message; // "Not Found" + * err.statusCode; // 404 * ``` */ /*#__NO_SIDE_EFFECTS__*/ export function createCustomError( name: string, constructCb?: ((self: any, args: IArguments) => void) | null, - errorBase?: B): T { + errorBase?: B, + superArgsFn?: ((args: IArguments) => ArrayLike) | null): T { let theBaseClass = errorBase || Error; let orgName = theBaseClass[PROTOTYPE][NAME]; @@ -131,7 +163,7 @@ export function createCustomError { assert.ok(theError instanceof ApplicationError, "Check that the startupError is an ApplicationError"); }); }); + + describe("createCustomError with superArgsFn", () => { + it("reorders arguments before passing to base class", () => { + interface HttpErrorConstructor extends CustomErrorConstructor { + new(statusCode: number, message: string): HttpError; + (statusCode: number, message: string): HttpError; + } + + interface HttpError extends Error { + readonly statusCode: number; + } + + let MyHttpError = createCustomError("HttpError", + (self, args) => { + self.statusCode = args[0]; + }, + Error, + (args) => [ args[1] ] // pass only the message to base Error constructor + ); + + let err = _expectThrow(() => { + throw new MyHttpError(404, "Not Found"); + }, "Not Found"); + + assert.ok(err instanceof Error, "The custom error is an Error"); + assert.ok(isError(err), "isError returns true"); + assert.equal(err.name, "HttpError", "Name is HttpError"); + assert.equal(err.message, "Not Found", "Message is set from second arg"); + assert.equal(err.statusCode, 404, "statusCode is set from first arg"); + }); + + it("passes subset of arguments to base class", () => { + interface DetailedErrorConstructor extends CustomErrorConstructor { + new(message: string, code: number, detail: string): DetailedError; + (message: string, code: number, detail: string): DetailedError; + } + + interface DetailedError extends Error { + readonly code: number; + readonly detail: string; + } + + let MyDetailedError = createCustomError("DetailedError", + (self, args) => { + self.code = args[1]; + self.detail = args[2]; + }, + Error, + (args) => [ args[0] ] // pass only message to base Error + ); + + let err = _expectThrow(() => { + throw new MyDetailedError("Something failed", 42, "extra detail"); + }, "Something failed"); + + assert.ok(err instanceof Error, "The custom error is an Error"); + assert.ok(isError(err), "isError returns true"); + assert.equal(err.name, "DetailedError", "Name is DetailedError"); + assert.equal(err.message, "Something failed", "Message is set correctly"); + assert.equal(err.code, 42, "code is set correctly"); + assert.equal(err.detail, "extra detail", "detail is set correctly"); + }); + + it("works with custom error base class", () => { + interface AppErrorConstructor extends CustomErrorConstructor { + new(message: string): AppError; + (message: string): AppError; + } + interface AppError extends Error {} + + interface ServiceErrorConstructor extends CustomErrorConstructor { + new(service: string, message: string): ServiceError; + (service: string, message: string): ServiceError; + } + interface ServiceError extends AppError { + readonly service: string; + } + + let AppErrorCls = createCustomError("AppError"); + let ServiceErrorCls = createCustomError("ServiceError", + (self, args) => { + self.service = args[0]; + }, + AppErrorCls, + (args) => [ args[1] ] // pass message to AppError base class + ); + + let err = _expectThrow(() => { + throw new ServiceErrorCls("auth-service", "Unauthorized"); + }, "Unauthorized"); + + assert.ok(err instanceof Error, "is an Error"); + assert.ok(err instanceof AppErrorCls, "is an AppError"); + assert.ok(err instanceof ServiceErrorCls, "is a ServiceError"); + assert.ok(isError(err), "isError returns true"); + assert.equal(err.name, "ServiceError", "Name is ServiceError"); + assert.equal(err.message, "Unauthorized", "Message is set from second arg"); + assert.equal(err.service, "auth-service", "service is set from first arg"); + }); + + it("null superArgsFn behaves like no superArgsFn (passes all args)", () => { + interface MyErrorConstructor extends CustomErrorConstructor { + new(message: string): MyError; + (message: string): MyError; + } + interface MyError extends Error {} + + let MyErrorCls = createCustomError("MyNullSuperError", null, Error, null); + + let err = _expectThrow(() => { + throw new MyErrorCls("hello"); + }, "hello"); + + assert.ok(isError(err), "isError returns true"); + assert.equal(err.message, "hello", "Message is set correctly"); + }); + }); });