diff --git a/README.md b/README.md index d83c48b..8706c96 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,12 @@ const range = def(sig1, sig2, (startOrStop, stop?, step = 1) => { const unwrappedRange = range.unwrap(); // ^?: ((stop: number) => number[]) & ((start: number, stop: number, step?: number) => number[]) +// Use `onValidationError` to provide alternative error handling instead of just throwing a `TypeError` +const range2 = range.onValidationError(console.error); +range2(1, 3.5); // => [1, 2, 3] - The function is returned as the errors are handled by a custom handler +// This time, the error message is printed to the console and no error is thrown +// If you still want to throw an error instead of returning the function, you can rethrow it in the custom handler + // Use `Safunc#matchArguments` to get the matched `Sig` for the given arguments range.matchArguments(3); // => sig1 range.matchArguments(1, 5); // => sig2 diff --git a/package.json b/package.json index a88c1f8..e69a018 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safunc", - "version": "0.1.2", + "version": "0.1.3", "private": true, "description": "Create runtime-validated functions for both synchronous and asynchronous ones with ease, supporting optional parameters and overloaded signatures with smart type inference in TypeScript", "keywords": [ diff --git a/src/safunc.spec.ts b/src/safunc.spec.ts index aaad4dc..c071016 100644 --- a/src/safunc.spec.ts +++ b/src/safunc.spec.ts @@ -358,6 +358,34 @@ it("should support asynchronous functions", async () => { ), ); + let errorMessage = ""; + const getTodosWrong2 = getTodosWrong.onValidationError((e) => { + errorMessage = e.message; + }); + expect((await getTodosWrong2()).slice(0, 3)).toEqual([ + { + userId: 1, + id: 1, + title: "delectus aut autem", + completed: false, + }, + { + userId: 1, + id: 2, + title: "quis ut nam facilis et officia qui", + completed: false, + }, + { + userId: 1, + id: 3, + title: "fugiat veniam minus", + completed: false, + }, + ]); + expect(errorMessage).toBe( + "Property '0/id' of the return value of 'function(): Promise0; id: string>0; title: string; completed: boolean }>>' must be a string (was number)", + ); + const getTodo = defAsync( // ^? sig("integer>0", "=>", todo), diff --git a/src/safunc.ts b/src/safunc.ts index 67e24bf..9fa2f64 100644 --- a/src/safunc.ts +++ b/src/safunc.ts @@ -288,6 +288,13 @@ export interface Safunc extends F { */ unwrap: () => F; + /** + * Provides an error handler for validation errors instead of throwing them. + * @param handler The error handler. + * @returns + */ + onValidationError: (handler: (e: TypeError) => void) => Safunc; + /** * Get the matched `Sig` for given arguments. * @param args Arguments to match. @@ -311,7 +318,13 @@ export interface Safunc extends F { } const _defBuilder = - ({ async }: { async: boolean }) => + ({ + async, + validationErrorHandler, + }: { + async: boolean; + validationErrorHandler?: (e: TypeError) => void; + }) => (...args: unknown[]) => { const sigs = args.slice(0, -1) as Sig[]; const fn = args[args.length - 1] as Fn; @@ -323,6 +336,11 @@ const _defBuilder = ].sort(); if (!availableArgumentLengths.includes(args.length)) { const message = `Expected ${humanizeNaturalNumbers(availableArgumentLengths)} arguments, but got ${args.length}`; + if (validationErrorHandler) { + validationErrorHandler(new TypeError(message)); + $matchedMorphedArguments = args; + return sigs[0]!; + } throw new TypeError(message); } @@ -374,7 +392,14 @@ const _defBuilder = .map(([sig, message], i) => ({ i, sig, message })) .filter(({ message: m }) => m !== "ARG_LENGTH_NOT_MATCH"); - if (errors.length === 1) throw new TypeError(errors[0]!.message); + if (errors.length === 1) { + if (validationErrorHandler) { + validationErrorHandler(new TypeError(errors[0]!.message)); + $matchedMorphedArguments = args; + return errors[0]!.sig; + } + throw new TypeError(errors[0]!.message); + } let message = "No overload "; if (fn.name) message += `of function '${fn.name}' `; @@ -387,6 +412,11 @@ const _defBuilder = "\n"; } message = message.trimEnd(); + if (validationErrorHandler) { + validationErrorHandler(new TypeError(message)); + $matchedMorphedArguments = args; + return errors[0]!.sig; + } throw new TypeError(message); }; @@ -410,6 +440,10 @@ const _defBuilder = if (sigs.length > 1) message += `(overload ${sigs.indexOf(sig) + 1} of ${sigs.length}) `; message += reason; message = capitalize(message); + if (validationErrorHandler) { + validationErrorHandler(new TypeError(message)); + return r; + } throw new TypeError(message); }; @@ -439,6 +473,9 @@ const _defBuilder = unwrap: () => f, + onValidationError: (handler: (e: TypeError) => void) => + _defBuilder({ async, validationErrorHandler: handler })(...args) as unknown as Safunc, + matchArguments: (...args: unknown[]) => { try { return matchArguments(...args); diff --git a/test/README.spec.ts b/test/README.spec.ts index d331eea..e4ce82e 100644 --- a/test/README.spec.ts +++ b/test/README.spec.ts @@ -274,6 +274,15 @@ test("helper methods", () => { return Array.from({ length: Math.ceil((stop - start) / step) }, (_, i) => start + i * step); }); + let errorMessage = ""; + const range2 = range.onValidationError((e) => { + errorMessage = e.message; + }); + expect(range2(1, 3.5)).toEqual([1, 2, 3]); + expect(errorMessage).toBe( + "The 2nd argument of 'function(integer, integer, ?integer>0): Array' (overload 2 of 2) must be an integer (was 3.5)", + ); + expect(range.matchArguments(3)).toBe(sig1); expect(range.matchArguments(1, 5)).toBe(sig2); expect(range.matchArguments("foo")).toBe(null);