diff --git a/CHANGELOG.md b/CHANGELOG.md index a296a92..7ec8c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # lilbetter.js +## 0.1.0 + +### Minor Changes + +- ecfbfc7: Created the tryAsync (tryA) and trySync (tryS) functions, said can be used for wrapping and calling functions that can throw error. Said error can then be handled using in a Go-like fashion. + ## 0.0.3 ### Patch Changes diff --git a/package.json b/package.json index 68e56bf..39d212d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lilbetter.js", - "version": "0.0.3", + "version": "0.1.0", "description": "", "main": "./src/index.js", "browser": "./src/index.js", @@ -12,9 +12,16 @@ "url": "https://github.com/LoredDev/lilbetter.js" }, "exports": { - "import": "./src/index.js", - "default": "./src/index.js", - "types": "./src/index.d.ts" + ".": { + "import": "./src/index.js", + "default": "./src/index.js", + "types": "./src/index.d.ts" + }, + "./try": { + "import": "./src/try.js", + "default": "./src/try.js", + "types": "./src/try.d.ts" + } }, "files": [ "./src/**/*.js", @@ -36,7 +43,7 @@ }, "license": "MIT", "engines": { - "node": ">=20", + "node": ">=18", "pnpm": ">=8" }, "publishConfig": { diff --git a/src/index.d.ts b/src/index.d.ts index 40a0c5d..ddb1236 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,6 +1,3 @@ -interface Test { - name: string, -} +export * from './try.d.ts'; -export default Test; diff --git a/src/index.js b/src/index.js index f6a705a..36ae513 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,3 @@ -/* - * Placeholder file - */ -// eslint-disable-next-line no-console -console.log('Hello, world!'); + +export * from './try.js'; + diff --git a/src/try.d.ts b/src/try.d.ts new file mode 100644 index 0000000..4377547 --- /dev/null +++ b/src/try.d.ts @@ -0,0 +1,94 @@ + +/** + * The WrappedResult type returned by the wrapped function in {@link trySync} + * and {@link tryAsync}. + * + * **If a error occurred, the result is undefined**. + * If there's not a error, error will be null and the result will be defined. + */ +type WrappedResult = [R, null] | [undefined, Error]; + +/** + * The returned function from {@link tryAsync}. + * + * @param args - The arguments of the function. + * - The arguments of the wrapped function. + * @returns + * - The final tuple containing the Error object (if one occured) and the resulting value. + */ +type WrappedAsyncFunction = (...args: Parameters) => +Promise>>>; + +/** + * Function-sugar/Syntax-sugar for handling functions that can throw errors. Wrapping then + * into a try-catch block / "curried function" that returns a "tuple as array" of error and + * value, which can be used for handling the error using a Go-like fashion. **This function + * is for asynchronous operations,** for synchronous ones, see {@link trySync}. + * + * **If there's a error, the result is undefined**. + * If there's not a error, error will be null and the result will be defined. + * + * @param func + * - The function to be executed. + * @returns + * - The function to be immediately called with the wrapped function's arguments. + * @example + * const [error, res] = await tryAsync(fetch)("https://example.com"); + * if (error !== null) { + * // error handling... + * console.log(error); + * } + * // continue the logic... + */ +function tryAsync< + F extends (...args: Parameters) => (ReturnType extends Promise + ? ReturnType + : Promise> + ), +>(func: F): WrappedAsyncFunction; + +/** + * The returned function from {@link trySync}. + * + * @param args + * - The arguments of the wrapped function. + * @returns + * - The final tuple containing the Error object (if one occured) and the resulting value. + */ +type WrappedFunction = (...args: Parameters) => +WrappedResult>; + +/** + * Function-sugar/Syntax-sugar for handling functions that can throw errors. Wrapping then + * into a try-catch block / "curried function" that returns a "tuple as array" of error and + * value, which can be used for handling the error using a Go-like fashion. **This function + * is for synchronous operations,** for asynchronous ones, see {@link tryAsync}. + * + * **If there's a error, the result is undefined**. + * If there's not a error, error will be null and the result will be defined. + * + * @param func + * - The function to be executed. + * @returns + * - The function to be immediately called with the wrapped function's arguments. + * @example + * const [error, json] = trySync(JSON.parse)('{ "hello": "world" }'); + * if (error !== null) { + * // error handling... + * console.log(error); + * } + * // continue the logic... + */ +function trySync< + F extends (...args: Parameters) => ReturnType, +>(func: F): WrappedFunction; + +export { + type WrappedAsyncFunction, + type WrappedFunction, + type WrappedResult, + tryAsync as tryA, + tryAsync, + trySync as tryS, + trySync, +}; diff --git a/src/try.js b/src/try.js new file mode 100644 index 0000000..c78ee01 --- /dev/null +++ b/src/try.js @@ -0,0 +1,133 @@ +/* eslint-disable no-secrets/no-secrets */ + +/** + * The WrappedResult type returned by the wrapped function in {@link trySync} + * and {@link tryAsync}. + * + * **If a error occurred, the result is undefined**. + * If there's not a error, error will be null and the result will be defined. + * + * @typedef {[R, null] | [undefined, Error]} WrappedResult + * @template R + */ + +/** + * The returned function from {@link tryAsync}. + * + * @typedef {(...args: Parameters) => Promise>>>} + * WrappedAsyncFunction + * @template {(...args: Parameters) => ReturnType} F + */ + +/** + * Function-sugar/Syntax-sugar for handling functions that can throw errors. Wrapping then + * into a try-catch block / "curried function" that returns a "tuple as array" of error and + * value, which can be used for handling the error using a Go-like fashion. **This function + * is for asynchronous operations,** for synchronous ones, see {@link trySync}. + * + * **If there's a error, the result is undefined**. + * If there's not a error, error will be null and the result will be defined. + * + * @template {(...args: Parameters) => Promise>>} F + * @param {F} func + * - The function to be executed. + * @returns {WrappedAsyncFunction} + * - The function to be immediately called with the wrapped function's arguments. + * @example + * const [error, res] = await tryAsync(fetch)("https://example.com"); + * if (error !== null) { + * // error handling... + * console.log(error); + * } + * // continue the logic... + */ +function tryAsync(func) { + /** + * The returned function from {@link tryAsync}. + * + * @param {Parameters} args - The arguments of the function. + * - The arguments of the wrapped function. + * @returns {Promise>>>} + * - The final tuple containing the Error object (if one occured) and the resulting value. + */ + return async (...args) => { + try { + return [await func(...args), null]; + } + catch (error) { + if (error instanceof Error) return [undefined, error]; + + const errObj = new Error(error?.toString + // eslint-disable-next-line @typescript-eslint/no-base-to-string + ? `Stringified error to: ${error.toString()}` + : 'Could not stringify error', + { cause: { value: error } }); + + return [undefined, errObj]; + } + }; +} + +/** + * The returned function from {@link trySync}. + * + * @typedef {(...args: Parameters) => WrappedResult>} WrappedFunction + * @template {(...args: Parameters) => ReturnType} F + */ + +/** + * Function-sugar/Syntax-sugar for handling functions that can throw errors. Wrapping then + * into a try-catch block / "curried function" that returns a "tuple as array" of error and + * value, which can be used for handling the error using a Go-like fashion. **This function + * is for synchronous operations,** for asynchronous ones, see {@link tryAsync}. + * + * **If there's a error, the result is undefined**. + * If there's not a error, error will be null and the result will be defined. + * + * @template {(...args: Parameters) => ReturnType} F + * @param {F} func + * - The function to be executed. + * @returns {WrappedFunction} + * - The function to be immediately called with the wrapped function's arguments. + * @example + * const [error, json] = trySync(JSON.parse)('{ "hello": "world" }'); + * if (error !== null) { + * // error handling... + * console.log(error); + * } + * // continue the logic... + */ +function trySync(func) { + /** + * The returned function from {@link trySync}. + * + * @param {Parameters} args + * - The arguments of the wrapped function. + * @returns {WrappedResult>} + * - The final tuple containing the Error object (if one occured) and the resulting value. + */ + return (...args) => { + try { + return [func(...args), null]; + } + catch (error) { + if (error instanceof Error) return [undefined, error]; + + const errObj = new Error(error?.toString + // eslint-disable-next-line @typescript-eslint/no-base-to-string + ? `Stringified error to: ${error.toString()}` + : 'Could not stringify error', + { cause: { value: error } }); + + return [undefined, errObj]; + } + }; +} + +export { + tryAsync as tryA, + tryAsync, + trySync as tryS, + trySync, +}; + diff --git a/test/placeholder.test.js b/test/placeholder.test.js deleted file mode 100644 index c094da5..0000000 --- a/test/placeholder.test.js +++ /dev/null @@ -1,7 +0,0 @@ -// eslint-disable-next-line n/no-unpublished-import -import { expect, test } from 'vitest'; - -test('placeholder', () => { - expect(1).toBe(1); -}); - diff --git a/test/try.test.js b/test/try.test.js new file mode 100644 index 0000000..9f931ae --- /dev/null +++ b/test/try.test.js @@ -0,0 +1,35 @@ +/* eslint-disable import/no-relative-parent-imports */ +// eslint-disable-next-line n/no-unpublished-import +import { describe, it } from 'vitest'; + +import { tryA, tryS } from '../src/index.js'; + +describe.concurrent('Return values', () => { + it('JSON parsing [Sync, Success]', ({ expect }) => { + const [json, error] = tryS(JSON.parse)('{ "hello": "world" }'); + + expect(error).toBe(null); + expect(json).toEqual({ hello: 'world' }); + }); + it('JSON parsing [Sync, Error]', ({ expect }) => { + const [json, error] = tryS(JSON.parse)('{ "hello: "world" }'); + + expect(error?.name).toEqual('SyntaxError'); + expect(error).toBeInstanceOf(Error); + expect(json).toBe(undefined); + }); + it('Fetch function [Async, Success]', async ({ expect }) => { + const [res, error] = await tryA(fetch)('https://example.com'); + + expect(error).toBe(null); + expect(res?.status).toBe(200); + }); + it('Fetch function [Async, Error]', async ({ expect }) => { + const [res, error] = await tryA(fetch)('htps://example.com'); + + expect(error?.name).toEqual('TypeError'); + expect(error).toBeInstanceOf(Error); + expect(res).toBe(undefined); + }); +}); +