From 0bac01602dfd905e62dfa652c99a24d6972bdda6 Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 14:15:05 +0000 Subject: [PATCH 01/14] feat(resultsToResult): Cleanup --- .github/workflows/publish.yml | 1 + .gitignore | 1 + .oxfmtrc.json | 14 +++ README.md | 29 +++--- bun.lock | 44 +++++++++ index.test.ts | 142 ++++++++++++++++++++++++--- index.ts | 174 +++++++++++++++++++--------------- package.json | 23 +++-- 8 files changed, 315 insertions(+), 113 deletions(-) create mode 100644 .oxfmtrc.json diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 025d148..f3d1826 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,6 +20,7 @@ jobs: node-version: 24 - run: bun install --frozen-lockfile + - run: bun fmt - run: bun test - run: bun run build - run: npm publish diff --git a/.gitignore b/.gitignore index f06235c..82ee25a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +.idea diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..8d55b5b --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,14 @@ +{ + "printWidth": 60, + "experimentalSortImports": { + "groups": [ + ["side-effect"], + ["builtin"], + ["external", "external-type"], + ["internal", "internal-type"], + ["parent", "parent-type"], + ["sibling", "sibling-type"], + ["index", "index-type"] + ] + } +} diff --git a/README.md b/README.md index 8097193..7af9ab1 100644 --- a/README.md +++ b/README.md @@ -19,20 +19,23 @@ Create a safe function from an unsafe one: ```ts const getUser = n.safeFn( - async (id: string) => { - const res = await fetch(`https://example.com/users/${id}`); - if (!res.ok) return { success: false, error: "FAILED_TO_FETCH" }; - - return { success: true, data: await res.json() }; - }, - (err) => "FAILED_TO_GET_USER", + async (id: string) => { + const res = await fetch( + `https://example.com/users/${id}`, + ); + if (!res.ok) + return { success: false, error: "FAILED_TO_FETCH" }; + + return { success: true, data: await res.json() }; + }, + (err) => "FAILED_TO_GET_USER", ); const getUserResult = await getUser("some-user-id"); if (!getUserResult.success) { - console.error(getUserResult.error); + console.error(getUserResult.error); } else { - console.log(getUserResult.data); + console.log(getUserResult.data); } ``` @@ -43,12 +46,12 @@ Runs the provided callback function, catching any thrown errors and returning a ```ts const user = await n.fromUnsafe( - () => db.findUser("some-user-id"), - (err) => "FAILED_T0_FIND_USER", + () => db.findUser("some-user-id"), + (err) => "FAILED_T0_FIND_USER", ); if (!user.success) { - console.error(user.error); + console.error(user.error); } else { - console.log(user.data); + console.log(user.data); } ``` diff --git a/bun.lock b/bun.lock index 847fd65..e060fc4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "oh", "devDependencies": { "@types/bun": "latest", + "oxfmt": "0.32.0", }, "peerDependencies": { "typescript": "5", @@ -12,6 +14,44 @@ }, }, "packages": { + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.32.0", "", { "os": "android", "cpu": "arm" }, "sha512-DpVyuVzgLH6/MvuB/YD3vXO9CN/o9EdRpA0zXwe/tagP6yfVSFkFWkPqTROdqp0mlzLH5Yl+/m+hOrcM601EbA=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-w1cmNXf9zs0vKLuNgyUF3hZ9VUAS1hBmQGndYJv1OmcVqStBtRTRNxSWkWM0TMkrA9UbvIvM9gfN+ib4Wy6lkQ=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-m6wQojz/hn94XdZugFPtdFbOvXbOSYEqPsR2gyLyID3BvcrC2QsJyT1o3gb4BZEGtZrG1NiKVGwDRLM0dHd2mg=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hN966Uh6r3Erkg2MvRcrJWaB6QpBzP15rxWK/QtkUyD47eItJLsAQ2Hrm88zMIpFZ3COXZLuN3hqgSlUtvB0Xw=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g5UZPGt8tJj263OfSiDGdS54HPa0KgFfspLVAUivVSdoOgsk6DkwVS9nO16xQTDztzBPGxTvrby8WuufF0g86Q=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-F4ZY83/PVQo9ZJhtzoMqbmjqEyTVEZjbaw4x1RhzdfUhddB41ZB2Vrt4eZi7b4a4TP85gjPRHgQBeO0c1jbtaw=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-olR37eG16Lzdj9OBSvuoT5RxzgM5xfQEHm1OEjB3M7Wm4KWa5TDWIT13Aiy74GvAN77Hq1+kUKcGVJ/0ynf75g=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-eZhk6AIjRCDeLoXYBhMW7qq/R1YyVi+tGnGfc3kp7AZQrMsFaWtP/bgdCJCTNXMpbMwymtVz0qhSQvR5w2sKcg=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UYiqO9MlipntFbdbUKOIo84vuyzrK4TVIs7Etat91WNMFSW54F6OnHq08xa5ZM+K9+cyYMgQPXvYCopuP+LyKw=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.32.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-IDH/fxMv+HmKsMtsjEbXqhScCKDIYp38sgGEcn0QKeXMxrda67PPZA7HMfoUwEtFUG+jsO1XJxTrQsL+kQ90xQ=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.32.0", "", { "os": "linux", "cpu": "none" }, "sha512-bQFGPDa0buYWJFeK2I7ah8wRZjrAgamaG2OAGv+Ua5UMYEnHxmHcv+r8lWUUrwP2oqQGvp1SB8JIVtBbYuAueQ=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.32.0", "", { "os": "linux", "cpu": "none" }, "sha512-3vFp9DW1ItEKWltADzCFqG5N7rYFToT4ztlhg8wALoo2E2VhveLD88uAF4FF9AxD9NhgHDGmPCV+WZl/Qlj8cQ=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.32.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Fub2y8S9ImuPzAzpbgkoz/EVTWFFBolxFZYCMRhRZc8cJZI2gl/NlZswqhvJd/U0Jopnwgm/OJ2x128vVzFFWA=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XufwsnV3BF81zO2ofZvhT4FFaMmLTzZEZnC9HpFz/quPeg9C948+kbLlZnsfjmp+1dUxKMCpfmRMqOfF4AOLsA=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u2f9tC2qYfikKmA2uGpnEJgManwmk0ZXWs5BB4ga4KDu2JNLdA3i634DGHeMLK9wY9+iRf3t7IYpgN3OVFrvDw=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.32.0", "", { "os": "none", "cpu": "arm64" }, "sha512-5ZXb1wrdbZ1YFXuNXNUCePLlmLDy4sUt4evvzD4Cgumbup5wJgS9PIe5BOaLywUg9f1wTH6lwltj3oT7dFpIGA=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-IGSMm/Agq+IA0++aeAV/AGPfjcBdjrsajB5YpM3j7cMcwoYgUTi/k2YwAmsHH3ueZUE98pSM/Ise2J7HtyRjOA=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.32.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-H/9gsuqXmceWMsVoCPZhtJG2jLbnBeKr7xAXm2zuKpxLVF7/2n0eh7ocOLB6t+L1ARE76iORuUsRMnuGjj8FjQ=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fF8VIOeligq+mA6KfKvWtFRXbf0EFy73TdR6ZnNejdJRM8VWN1e3QFhYgIwD7O8jBrQsd7EJbUpkAr/YlUOokg=="], + "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="], @@ -22,6 +62,10 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "oxfmt": ["oxfmt@0.32.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.32.0", "@oxfmt/binding-android-arm64": "0.32.0", "@oxfmt/binding-darwin-arm64": "0.32.0", "@oxfmt/binding-darwin-x64": "0.32.0", "@oxfmt/binding-freebsd-x64": "0.32.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.32.0", "@oxfmt/binding-linux-arm-musleabihf": "0.32.0", "@oxfmt/binding-linux-arm64-gnu": "0.32.0", "@oxfmt/binding-linux-arm64-musl": "0.32.0", "@oxfmt/binding-linux-ppc64-gnu": "0.32.0", "@oxfmt/binding-linux-riscv64-gnu": "0.32.0", "@oxfmt/binding-linux-riscv64-musl": "0.32.0", "@oxfmt/binding-linux-s390x-gnu": "0.32.0", "@oxfmt/binding-linux-x64-gnu": "0.32.0", "@oxfmt/binding-linux-x64-musl": "0.32.0", "@oxfmt/binding-openharmony-arm64": "0.32.0", "@oxfmt/binding-win32-arm64-msvc": "0.32.0", "@oxfmt/binding-win32-ia32-msvc": "0.32.0", "@oxfmt/binding-win32-x64-msvc": "0.32.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-KArQhGzt/Y8M1eSAX98Y8DLtGYYDQhkR55THUPY5VNcpFQ+9nRZkL3ULXhagHMD2hIvjy8JSeEQEP5/yYJSrLA=="], + + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], diff --git a/index.test.ts b/index.test.ts index cf92fdd..a638cef 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,5 +1,11 @@ -import { describe, expect, it } from "bun:test"; -import { n } from "."; +import { + describe, + expect, + expectTypeOf, + it, +} from "bun:test"; + +import { n, Result } from "."; describe("safeFn", () => { it("should catch any thrown errors and return success false", async () => { @@ -13,18 +19,20 @@ describe("safeFn", () => { }); it("should call and return the value from the error handler if an error is thrown", async () => { - const expectedErrorMessage = "an-unknown-error-occured" as const; + const expectedErrorMessage = + "an-unknown-error-occured" as const; const safeFunction = n.safeFn( async () => { throw new Error("Unexpected error."); }, - () => expectedErrorMessage + () => expectedErrorMessage, ); const result = await safeFunction(); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); expect(result.error).toBe(expectedErrorMessage); @@ -39,7 +47,8 @@ describe("safeFn", () => { const result = await safeFunction(); - if (!result.success) throw new Error("Result should be success."); + if (!result.success) + throw new Error("Result should be success."); expect(result.success).toBe(true); expect(result.data).toBe(expectedData); @@ -54,7 +63,8 @@ describe("safeFn", () => { const result = await safeFunction(); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); expect(result.error).toBe(expectedError); @@ -69,7 +79,8 @@ describe("safeFn", () => { const result = await safeFunction(expectedName); - if (!result.success) throw new Error("Result should be success."); + if (!result.success) + throw new Error("Result should be success."); expect(result.success).toBe(true); expect(result.data).toBe(expectedName); @@ -81,7 +92,8 @@ describe("fromUnsafe", () => { const expectedReturn = "some result"; const result = await n.fromUnsafe(() => expectedReturn); - if (!result.success) throw new Error("Result should be success."); + if (!result.success) + throw new Error("Result should be success."); expect(result.success).toBe(true); expect(result.data).toBe(expectedReturn); @@ -89,10 +101,12 @@ describe("fromUnsafe", () => { it("should handle synchronous errors", async () => { const result = n.fromUnsafe(() => { - if (true as boolean) throw new Error("Some synchronous error"); + if (true as boolean) + throw new Error("Some synchronous error"); }); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); }); @@ -102,7 +116,8 @@ describe("fromUnsafe", () => { throw new Error("Some synchronous error"); }); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); }); @@ -115,7 +130,7 @@ describe("fromUnsafe", () => { async () => { throw thrownError; }, - (e) => (originalError = e) + (e) => (originalError = e), ); expect(originalError).toBe(thrownError); @@ -128,12 +143,109 @@ describe("fromUnsafe", () => { async () => { throw new Error("Some synchronous error"); }, - () => expectedError + () => expectedError, ); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); expect(result.error).toBe(expectedError); }); }); + +describe("resultsToResult", () => { + it("should return success false if a single result is success false", () => { + const results = [ + { success: true, data: "some data" as const }, + { success: false, error: "SOME_ERROR" as const }, + { + success: false, + error: "SOME_OTHER_ERROR" as const, + }, + ] satisfies Result[]; + + const result = n.resultsToResult(results); + + if (result.success) { + expectTypeOf(result).toMatchObjectType<{ + success: true; + data: { + success: true; + data: "some data"; + error?: undefined; + }[]; + }>(); + } + + if (!result.success) { + expectTypeOf(result).toMatchObjectType<{ + success: false; + error: ( + | { + success: false; + error: "SOME_ERROR"; + data?: undefined; + } + | { + success: false; + error: "SOME_OTHER_ERROR"; + data?: undefined; + } + )[]; + }>(); + + expect(result.success).toBe(false); + expect(result.error).toMatchObject([ + { success: false, error: "SOME_ERROR" as const }, + { + success: false, + error: "SOME_OTHER_ERROR" as const, + }, + ]); + } + }); + + it("should return success true if there are no success false results", () => { + const results = [ + { success: true, data: "some data" as const }, + { success: true, data: "other data" as const }, + ] satisfies Result[]; + + const result = n.resultsToResult(results); + + if (!result.success) { + expectTypeOf(result).toMatchObjectType<{ + success: false; + error: never[]; + }>(); + throw new Error("Should be success result."); + } + + expectTypeOf(result).toMatchObjectType<{ + success: true; + data: ( + | { + success: true; + data: "some data"; + } + | { + success: true; + data: "other data"; + } + )[]; + }>(); + + expect(result.success).toBe(true); + expect(result.data).toMatchObject([ + { + success: true, + data: "some data", + }, + { + success: true, + data: "other data", + }, + ]); + }); +}); diff --git a/index.ts b/index.ts index 00278f1..24cabf4 100644 --- a/index.ts +++ b/index.ts @@ -1,14 +1,9 @@ - - export type Result = - | { - success: true; - data: T; - } - | { success: false; error: E }; - -type DataOf = R extends { success: true; data: infer D } ? D : never -type ErrorOf = R extends { success: false; error: infer E } ? E : never + | { + success: true; + data: T; + } + | { success: false; error: E }; /** * Create a safe function from an unsafe one. @@ -36,31 +31,31 @@ type ErrorOf = R extends { success: false; error: infer E } ? * } */ function safeFn< - T extends Result | Promise, - A extends unknown[], - E = null, + T extends Result | Promise, + A extends unknown[], + E = null, >( - cb: (...args: A) => T, - eh?: (e: unknown) => E, + cb: (...args: A) => T, + eh?: (e: unknown) => E, ): (...args: A) => T | Result { - const createErrorResult = (e: unknown) => - ({ - success: false, - error: eh?.(e) ?? null, - }) as const; - - return (...args) => { - try { - const result = cb(...args); - - if (result instanceof Promise) - return result.catch(createErrorResult) as T; - - return result; - } catch (e) { - return createErrorResult(e) as T; - } - }; + const createErrorResult = (e: unknown) => + ({ + success: false, + error: eh?.(e) ?? null, + }) as const; + + return (...args) => { + try { + const result = cb(...args); + + if (result instanceof Promise) + return result.catch(createErrorResult) as T; + + return result; + } catch (e) { + return createErrorResult(e) as T; + } + }; } /** @@ -79,51 +74,78 @@ function safeFn< * } */ function fromUnsafe< - T, - E = null, - R = T extends Promise - ? Promise, E>> - : Result, + T, + E = null, + R = T extends Promise + ? Promise, E>> + : Result, >(cb: () => T, eh?: (err: unknown) => E): R { - const createErrorResult = (e: unknown) => ({ - success: false, - error: eh?.(e) ?? null, - }); - - const createSuccessResult = (data: T) => - ({ - success: true, - data, - }) as const; - - try { - const result = cb(); - - if (result instanceof Promise) - return result.then(createSuccessResult).catch(createErrorResult) as R; - - return createSuccessResult(result) as R; - } catch (e) { - return createErrorResult(e) as R; - } + const createErrorResult = (e: unknown) => ({ + success: false, + error: eh?.(e) ?? null, + }); + + const createSuccessResult = (data: T) => + ({ + success: true, + data, + }) as const; + + try { + const result = cb(); + + if (result instanceof Promise) + return result + .then(createSuccessResult) + .catch(createErrorResult) as R; + + return createSuccessResult(result) as R; + } catch (e) { + return createErrorResult(e) as R; + } } -function resultsToResult(results: R): Result[], ErrorOf[]> { - let success: boolean = true - - const error: any[] = [] - const data: any[] = [] - - for (const result of results) { - if (!result.success) { - success = false - error.push(result.error) - } else { - data.push(result.data) - } - } - - return success ? { success: true, data } : { success: false, error } -} +/** + * Convert a list of results into a single result. + * + * @param results - A list of Results. + * @returns A single result containing the data / errors of the input results. + * + * @example + * const findUserResults = userIds.map((userId) => + * n.fromUnsafe( + * () => db.findUser(userId), + * () => "FAILED_TO_FIND_USER" as const, + * ), + * ); + * + * const result = n.resultsToResult(findUserResults) + */ +const resultsToResult = < + T extends Result[], + D extends Extract, + E extends Extract, +>( + results: T, +): Result => { + const errors = results.filter( + (result): result is E => !result.success, + ); + + if (errors.length) + return { + success: false, + error: errors, + }; + + const successes = results.filter( + (result): result is D => result.success, + ); + + return { + success: true, + data: successes, + }; +}; export const n = { safeFn, fromUnsafe, resultsToResult }; diff --git a/package.json b/package.json index 3a04b29..b7fda6d 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,26 @@ { "name": "neverpanic", - "module": "index.ts", - "version": "0.0.5", - "type": "module", + "version": "1.0.0-beta.1", "repository": { "url": "https://github.com/bgrcs/neverpanic" }, - "scripts": { - "build": "tsc", - "test": "bun test" - }, - "files": ["dist"], + "files": [ + "dist" + ], + "type": "module", + "module": "index.ts", "exports": { ".": "./dist/index.js" }, + "scripts": { + "build": "tsc", + "test": "bun test", + "fmt": "oxfmt --check", + "fmt:fix": "oxfmt --write" + }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "oxfmt": "0.32.0" }, "peerDependencies": { "typescript": "5" From b9841f1e96abc7fe306c35fd45c06a5039191388 Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 14:19:37 +0000 Subject: [PATCH 02/14] ci: Tag with beta --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f3d1826..05c579e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,4 +23,4 @@ jobs: - run: bun fmt - run: bun test - run: bun run build - - run: npm publish + - run: npm publish --tag beta From 8c73b8583823c02ce924aee2407f72978458cb93 Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 15:04:25 +0000 Subject: [PATCH 03/14] ci: Add ok & err helpers --- index.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 24cabf4..597f95d 100644 --- a/index.ts +++ b/index.ts @@ -5,6 +5,16 @@ export type Result = } | { success: false; error: E }; +const ok = (data: T): Result => ({ + success: true, + data, +}); + +const err = (error: T): Result => ({ + success: false, + error, +}); + /** * Create a safe function from an unsafe one. * @@ -37,7 +47,7 @@ function safeFn< >( cb: (...args: A) => T, eh?: (e: unknown) => E, -): (...args: A) => T | Result { +): (...args: A) => T | { success: false; error: E } { const createErrorResult = (e: unknown) => ({ success: false, @@ -148,4 +158,10 @@ const resultsToResult = < }; }; -export const n = { safeFn, fromUnsafe, resultsToResult }; +export const n = { + ok, + err, + safeFn, + fromUnsafe, + resultsToResult, +}; From fc5bdc562a5537154c3ebb04344ca8896e97e41a Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 15:17:51 +0000 Subject: [PATCH 04/14] ci: Make eh required --- index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.ts b/index.ts index 597f95d..739b36e 100644 --- a/index.ts +++ b/index.ts @@ -46,12 +46,12 @@ function safeFn< E = null, >( cb: (...args: A) => T, - eh?: (e: unknown) => E, + eh: (e: unknown) => E, ): (...args: A) => T | { success: false; error: E } { const createErrorResult = (e: unknown) => ({ success: false, - error: eh?.(e) ?? null, + error: eh(e), }) as const; return (...args) => { @@ -89,10 +89,10 @@ function fromUnsafe< R = T extends Promise ? Promise, E>> : Result, ->(cb: () => T, eh?: (err: unknown) => E): R { +>(cb: () => T, eh: (err: unknown) => E): R { const createErrorResult = (e: unknown) => ({ success: false, - error: eh?.(e) ?? null, + error: eh(e), }); const createSuccessResult = (data: T) => From a0095a8772553d9f152cd90f5ee8368baa2f0b22 Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 16:12:06 +0000 Subject: [PATCH 05/14] ci: Get wrapped bro --- index.ts | 251 +++++++++++++++++++++++++------------------------------ 1 file changed, 114 insertions(+), 137 deletions(-) diff --git a/index.ts b/index.ts index 739b36e..afe6496 100644 --- a/index.ts +++ b/index.ts @@ -5,163 +5,140 @@ export type Result = } | { success: false; error: E }; -const ok = (data: T): Result => ({ - success: true, - data, -}); - -const err = (error: T): Result => ({ - success: false, - error, -}); - /** - * Create a safe function from an unsafe one. + * Create a neverpanic instance with strictly typed Error and Data constraints. * - * @param cb - The async function to wrap. - * @param [eh] - Optional fallback error handler. - * @returns A new function that returns a typesafe Result. + * @returns A neverpanic instance with all functions constrained to the specified types. * * @example - * const getUser = n.safeFn( - * async (id: string) => { - * const res = await fetch(`https://example.com/users/${id}`); - * if (!res.ok) return { success: false, error: "FAILED_TO_FETCH" }; + * type MyData = { id: string; name: string }; + * type MyError = "NOT_FOUND" | "UNAUTHORIZED" | "SERVER_ERROR"; + * + * const myN = createNeverpanic(); * - * return { success: true, data: await res.json() }; - * }, - * () => "FAILED_TO_GET_USER" - * ); + * // Now myN.ok only accepts MyData + * const result = myN.ok({ id: "123", name: "John" }); * - * const getUserResult = await getUser("some-user-id"); - * if (!getUserResult.success) { - * console.error(getUserResult.error); - * } else { - * console.log(getUserResult.data); - * } + * // And myN.err only accepts MyError + * const error = myN.err("NOT_FOUND"); */ -function safeFn< - T extends Result | Promise, - A extends unknown[], - E = null, ->( - cb: (...args: A) => T, - eh: (e: unknown) => E, -): (...args: A) => T | { success: false; error: E } { - const createErrorResult = (e: unknown) => - ({ +export function createNeverpanic< + D = unknown, + E = unknown, +>() { + const ok = ( + data: T, + ): Result => ({ + success: true, + data, + }); + + const err = ( + error: T, + ): Result => ({ + success: false, + error, + }); + + function safeFn< + T extends Result | Promise>, + A extends unknown[], + EH extends E = E, + >( + cb: (...args: A) => T, + eh: (e: unknown) => EH, + ): (...args: A) => T | { success: false; error: EH } { + const createErrorResult = (e: unknown) => + ({ + success: false, + error: eh(e), + }) as const; + + return (...args) => { + try { + const result = cb(...args); + + if (result instanceof Promise) + return result.catch(createErrorResult) as T; + + return result; + } catch (e) { + return createErrorResult(e) as { + success: false; + error: EH; + }; + } + }; + } + + function fromUnsafe< + T, + EH extends E = E, + R = T extends Promise + ? Promise> + : T extends D + ? Result + : never, + >(cb: () => T, eh: (err: unknown) => EH): R { + const createErrorResult = (e: unknown) => ({ success: false, error: eh(e), - }) as const; + }); + + const createSuccessResult = (data: T) => + ({ + success: true, + data, + }) as const; - return (...args) => { try { - const result = cb(...args); + const result = cb(); if (result instanceof Promise) - return result.catch(createErrorResult) as T; + return result + .then(createSuccessResult) + .catch(createErrorResult) as R; - return result; + return createSuccessResult(result) as R; } catch (e) { - return createErrorResult(e) as T; + return createErrorResult(e) as R; } - }; -} - -/** - * Run an unsafe function, handle any errors and return a Result. - * - * @param cb - The async function to call. - * @param [eh] - Optional fallback error handler. - * @returns The awaited return value of cb. - * - * @example - * const user = await n.fromUnsafe(() => db.findUser('some-user-id'), () => 'FAILED_T0_FIND_USER') - * if (!user.success) { - * console.error(user.error) - * } else { - * console.log(user.data) - * } - */ -function fromUnsafe< - T, - E = null, - R = T extends Promise - ? Promise, E>> - : Result, ->(cb: () => T, eh: (err: unknown) => E): R { - const createErrorResult = (e: unknown) => ({ - success: false, - error: eh(e), - }); - - const createSuccessResult = (data: T) => - ({ - success: true, - data, - }) as const; - - try { - const result = cb(); - - if (result instanceof Promise) - return result - .then(createSuccessResult) - .catch(createErrorResult) as R; - - return createSuccessResult(result) as R; - } catch (e) { - return createErrorResult(e) as R; } -} -/** - * Convert a list of results into a single result. - * - * @param results - A list of Results. - * @returns A single result containing the data / errors of the input results. - * - * @example - * const findUserResults = userIds.map((userId) => - * n.fromUnsafe( - * () => db.findUser(userId), - * () => "FAILED_TO_FIND_USER" as const, - * ), - * ); - * - * const result = n.resultsToResult(findUserResults) - */ -const resultsToResult = < - T extends Result[], - D extends Extract, - E extends Extract, ->( - results: T, -): Result => { - const errors = results.filter( - (result): result is E => !result.success, - ); - - if (errors.length) + const resultsToResult = < + T extends Result[], + TD extends Extract, + TE extends Extract, + >( + results: T, + ): Result => { + const errors = results.filter( + (result): result is TE => !result.success, + ); + + if (errors.length) + return { + success: false, + error: errors, + }; + + const successes = results.filter( + (result): result is TD => result.success, + ); + return { - success: false, - error: errors, + success: true, + data: successes, }; - - const successes = results.filter( - (result): result is D => result.success, - ); + }; return { - success: true, - data: successes, + ok, + err, + safeFn, + fromUnsafe, + resultsToResult, }; -}; - -export const n = { - ok, - err, - safeFn, - fromUnsafe, - resultsToResult, -}; +} + +export const n = createNeverpanic(); From 1df413142c759f952aa67fdef4e4612ad28cb6cb Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 16:23:02 +0000 Subject: [PATCH 06/14] ci: wrap --- index.test.ts | 63 ++++++++++++++++++++++++++++++++++----------------- index.ts | 40 ++++++++++++-------------------- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/index.test.ts b/index.test.ts index a638cef..38412bd 100644 --- a/index.test.ts +++ b/index.test.ts @@ -9,9 +9,12 @@ import { n, Result } from "."; describe("safeFn", () => { it("should catch any thrown errors and return success false", async () => { - const safeFunction = n.safeFn(async () => { - throw new Error("Unexpected error."); - }); + const safeFunction = n.safeFn( + async () => { + throw new Error("Unexpected error."); + }, + (originalError) => originalError, + ); const result = await safeFunction(); @@ -41,9 +44,12 @@ describe("safeFn", () => { it("should return the success result of the callback if it doesn't throw", async () => { const expectedData = "some data" as const; - const safeFunction = n.safeFn(async () => { - return { success: true, data: expectedData }; - }); + const safeFunction = n.safeFn( + async () => { + return { success: true, data: expectedData }; + }, + (originalError) => originalError, + ); const result = await safeFunction(); @@ -57,9 +63,12 @@ describe("safeFn", () => { it("should return the error result of the callback if it doesn't throw", async () => { const expectedError = "some-error" as const; - const safeFunction = n.safeFn(async () => { - return { success: false, error: expectedError }; - }); + const safeFunction = n.safeFn( + async () => { + return { success: false, error: expectedError }; + }, + (originalError) => originalError, + ); const result = await safeFunction(); @@ -73,9 +82,12 @@ describe("safeFn", () => { it("should pass arguments to the callback", async () => { const expectedName = "Bob"; - const safeFunction = n.safeFn(async (name: string) => { - return { success: true, data: name }; - }); + const safeFunction = n.safeFn( + async (name: string) => { + return { success: true, data: name }; + }, + (originalError) => originalError, + ); const result = await safeFunction(expectedName); @@ -88,9 +100,12 @@ describe("safeFn", () => { }); describe("fromUnsafe", () => { - it("should return the value from the callback", async () => { + it("should return the value from the callback", () => { const expectedReturn = "some result"; - const result = await n.fromUnsafe(() => expectedReturn); + const result = n.fromUnsafe( + () => expectedReturn, + (originalError) => originalError, + ); if (!result.success) throw new Error("Result should be success."); @@ -100,10 +115,13 @@ describe("fromUnsafe", () => { }); it("should handle synchronous errors", async () => { - const result = n.fromUnsafe(() => { - if (true as boolean) - throw new Error("Some synchronous error"); - }); + const result = n.fromUnsafe( + () => { + if (true as boolean) + throw new Error("Some synchronous error"); + }, + (originalError) => originalError, + ); if (result.success) throw new Error("Result should not be success."); @@ -112,9 +130,12 @@ describe("fromUnsafe", () => { }); it("should handle asynchronous errors", async () => { - const result = await n.fromUnsafe(async () => { - throw new Error("Some synchronous error"); - }); + const result = await n.fromUnsafe( + async () => { + throw new Error("Some synchronous error"); + }, + (originalError) => originalError, + ); if (result.success) throw new Error("Result should not be success."); diff --git a/index.ts b/index.ts index afe6496..b4473bf 100644 --- a/index.ts +++ b/index.ts @@ -5,27 +5,10 @@ export type Result = } | { success: false; error: E }; -/** - * Create a neverpanic instance with strictly typed Error and Data constraints. - * - * @returns A neverpanic instance with all functions constrained to the specified types. - * - * @example - * type MyData = { id: string; name: string }; - * type MyError = "NOT_FOUND" | "UNAUTHORIZED" | "SERVER_ERROR"; - * - * const myN = createNeverpanic(); - * - * // Now myN.ok only accepts MyData - * const result = myN.ok({ id: "123", name: "John" }); - * - * // And myN.err only accepts MyError - * const error = myN.err("NOT_FOUND"); - */ -export function createNeverpanic< +export const createNeverpanic = < D = unknown, E = unknown, ->() { +>() => { const ok = ( data: T, ): Result => ({ @@ -40,14 +23,16 @@ export function createNeverpanic< error, }); - function safeFn< + const safeFn = < T extends Result | Promise>, A extends unknown[], EH extends E = E, >( cb: (...args: A) => T, eh: (e: unknown) => EH, - ): (...args: A) => T | { success: false; error: EH } { + ): (( + ...args: A + ) => T | { success: false; error: EH }) => { const createErrorResult = (e: unknown) => ({ success: false, @@ -69,9 +54,9 @@ export function createNeverpanic< }; } }; - } + }; - function fromUnsafe< + const fromUnsafe = < T, EH extends E = E, R = T extends Promise @@ -79,7 +64,10 @@ export function createNeverpanic< : T extends D ? Result : never, - >(cb: () => T, eh: (err: unknown) => EH): R { + >( + cb: () => T, + eh: (err: unknown) => EH, + ): R => { const createErrorResult = (e: unknown) => ({ success: false, error: eh(e), @@ -103,7 +91,7 @@ export function createNeverpanic< } catch (e) { return createErrorResult(e) as R; } - } + }; const resultsToResult = < T extends Result[], @@ -139,6 +127,6 @@ export function createNeverpanic< fromUnsafe, resultsToResult, }; -} +}; export const n = createNeverpanic(); From 35015412727c9b2afc34fb2ca48b64ad9c47474d Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 20:25:33 +0000 Subject: [PATCH 07/14] chore: Typed instance --- index.test.ts | 23 ++++++++++-------- index.ts | 66 ++++++++++++++++----------------------------------- 2 files changed, 33 insertions(+), 56 deletions(-) diff --git a/index.test.ts b/index.test.ts index 38412bd..39a41c0 100644 --- a/index.test.ts +++ b/index.test.ts @@ -13,7 +13,7 @@ describe("safeFn", () => { async () => { throw new Error("Unexpected error."); }, - (originalError) => originalError, + (originalError) => n.err(originalError), ); const result = await safeFunction(); @@ -29,7 +29,7 @@ describe("safeFn", () => { async () => { throw new Error("Unexpected error."); }, - () => expectedErrorMessage, + () => n.err(expectedErrorMessage), ); const result = await safeFunction(); @@ -48,7 +48,7 @@ describe("safeFn", () => { async () => { return { success: true, data: expectedData }; }, - (originalError) => originalError, + (originalError) => n.err(originalError), ); const result = await safeFunction(); @@ -67,7 +67,7 @@ describe("safeFn", () => { async () => { return { success: false, error: expectedError }; }, - (originalError) => originalError, + (originalError) => n.err(originalError), ); const result = await safeFunction(); @@ -86,7 +86,7 @@ describe("safeFn", () => { async (name: string) => { return { success: true, data: name }; }, - (originalError) => originalError, + (originalError) => n.err(originalError), ); const result = await safeFunction(expectedName); @@ -104,7 +104,7 @@ describe("fromUnsafe", () => { const expectedReturn = "some result"; const result = n.fromUnsafe( () => expectedReturn, - (originalError) => originalError, + (originalError) => n.err(originalError), ); if (!result.success) @@ -120,7 +120,7 @@ describe("fromUnsafe", () => { if (true as boolean) throw new Error("Some synchronous error"); }, - (originalError) => originalError, + (originalError) => n.err(originalError), ); if (result.success) @@ -134,7 +134,7 @@ describe("fromUnsafe", () => { async () => { throw new Error("Some synchronous error"); }, - (originalError) => originalError, + (originalError) => n.err(originalError), ); if (result.success) @@ -151,7 +151,10 @@ describe("fromUnsafe", () => { async () => { throw thrownError; }, - (e) => (originalError = e), + (e) => { + originalError = e; + return n.err(originalError); + }, ); expect(originalError).toBe(thrownError); @@ -164,7 +167,7 @@ describe("fromUnsafe", () => { async () => { throw new Error("Some synchronous error"); }, - () => expectedError, + (originalError) => n.err(expectedError), ); if (result.success) diff --git a/index.ts b/index.ts index b4473bf..ae18c87 100644 --- a/index.ts +++ b/index.ts @@ -23,73 +23,47 @@ export const createNeverpanic = < error, }); - const safeFn = < - T extends Result | Promise>, - A extends unknown[], - EH extends E = E, - >( - cb: (...args: A) => T, - eh: (e: unknown) => EH, - ): (( - ...args: A - ) => T | { success: false; error: EH }) => { - const createErrorResult = (e: unknown) => - ({ - success: false, - error: eh(e), - }) as const; - - return (...args) => { + const safeFn = + < + T extends Result | Promise>, + A extends unknown[], + EH extends Result, + >( + cb: (...args: A) => T, + eh: (e: unknown) => EH, + ): ((...args: A) => T | EH) => + (...args) => { try { const result = cb(...args); if (result instanceof Promise) - return result.catch(createErrorResult) as T; + return result.catch(eh) as T; return result; } catch (e) { - return createErrorResult(e) as { - success: false; - error: EH; - }; + return eh(e); } }; - }; const fromUnsafe = < - T, - EH extends E = E, + T extends D | Promise, + EH extends Result, R = T extends Promise - ? Promise> - : T extends D - ? Result - : never, + ? Promise<{ success: true; data: U }> + : { success: true; data: T }, >( cb: () => T, eh: (err: unknown) => EH, - ): R => { - const createErrorResult = (e: unknown) => ({ - success: false, - error: eh(e), - }); - - const createSuccessResult = (data: T) => - ({ - success: true, - data, - }) as const; - + ): R | EH => { try { const result = cb(); if (result instanceof Promise) - return result - .then(createSuccessResult) - .catch(createErrorResult) as R; + return result.then(ok).catch(eh) as R; - return createSuccessResult(result) as R; + return ok(result as D) as R; } catch (e) { - return createErrorResult(e) as R; + return eh(e); } }; From 026a8da7150a6baa1d8abbecc2c68f75a9d3a4f0 Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 20:26:47 +0000 Subject: [PATCH 08/14] ci: Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7fda6d..ca70942 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neverpanic", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "repository": { "url": "https://github.com/bgrcs/neverpanic" }, From 366c97139879fc844903672d28320e4d54d760ad Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 21:06:09 +0000 Subject: [PATCH 09/14] chore: Add JSDOC --- index.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/index.ts b/index.ts index ae18c87..68ff316 100644 --- a/index.ts +++ b/index.ts @@ -23,6 +23,31 @@ export const createNeverpanic = < error, }); + /** + * Create a safe function from an unsafe one. + * + * @param cb - The async function to wrap. + * @param [eh] - Optional fallback error handler. + * @returns A new function that returns a typesafe Result. + * + * @example + * const getUser = n.safeFn( + * async (id: string) => { + * const res = await fetch(`https://example.com/users/${id}`); + * if (!res.ok) return { success: false, error: "FAILED_TO_FETCH" }; + * + * return { success: true, data: await res.json() }; + * }, + * () => "FAILED_TO_GET_USER" + * ); + * + * const getUserResult = await getUser("some-user-id"); + * if (!getUserResult.success) { + * console.error(getUserResult.error); + * } else { + * console.log(getUserResult.data); + * } + */ const safeFn = < T extends Result | Promise>, @@ -45,6 +70,21 @@ export const createNeverpanic = < } }; + /** + * Run an unsafe function, handle any errors and return a Result. + * + * @param cb - The async function to call. + * @param [eh] - Optional fallback error handler. + * @returns The awaited return value of cb. + * + * @example + * const user = await n.fromUnsafe(() => db.findUser('some-user-id'), () => 'FAILED_T0_FIND_USER') + * if (!user.success) { + * console.error(user.error) + * } else { + * console.log(user.data) + * } + */ const fromUnsafe = < T extends D | Promise, EH extends Result, @@ -67,6 +107,22 @@ export const createNeverpanic = < } }; + /** + * Convert a list of results into a single result. + * + * @param results - A list of Results. + * @returns A single result containing the data / errors of the input results. + * + * @example + * const findUserResults = userIds.map((userId) => + * n.fromUnsafe( + * () => db.findUser(userId), + * () => "FAILED_TO_FIND_USER" as const, + * ), + * ); + * + * const result = n.resultsToResult(findUserResults) + */ const resultsToResult = < T extends Result[], TD extends Extract, From 05b6fb3db6fe18a10d5fd9aed6dfb15780594e34 Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 23:28:27 +0000 Subject: [PATCH 10/14] chore: Add JSDOC --- index.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/index.ts b/index.ts index 68ff316..49260c8 100644 --- a/index.ts +++ b/index.ts @@ -5,6 +5,29 @@ export type Result = } | { success: false; error: E }; +/** + * Create a typesafe instance of neverpanic. + * + * @returns An instance of neverpanic that conforms to the types specified in the generic arguments. + * + * @example + * const getUser = n.safeFn( + * async (id: string) => { + * const res = await fetch(`https://example.com/users/${id}`); + * if (!res.ok) return { success: false, error: "FAILED_TO_FETCH" }; + * + * return { success: true, data: await res.json() }; + * }, + * () => "FAILED_TO_GET_USER" + * ); + * + * const getUserResult = await getUser("some-user-id"); + * if (!getUserResult.success) { + * console.error(getUserResult.error); + * } else { + * console.log(getUserResult.data); + * } + */ export const createNeverpanic = < D = unknown, E = unknown, From 1fa3309341a03976a550464264c6999aec9994fd Mon Sep 17 00:00:00 2001 From: liamd-psyomics Date: Sun, 22 Feb 2026 23:29:32 +0000 Subject: [PATCH 11/14] chore: Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca70942..df682f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neverpanic", - "version": "1.0.0-beta.2", + "version": "1.0.0-beta.3", "repository": { "url": "https://github.com/bgrcs/neverpanic" }, From 0ae0a9bb3d683dde3ba33991b6b9d7b368097cf2 Mon Sep 17 00:00:00 2001 From: bgrcs Date: Mon, 23 Feb 2026 12:04:02 +0000 Subject: [PATCH 12/14] feat: static ok err --- index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.ts b/index.ts index 49260c8..fca6eb0 100644 --- a/index.ts +++ b/index.ts @@ -34,15 +34,15 @@ export const createNeverpanic = < >() => { const ok = ( data: T, - ): Result => ({ - success: true, + ): { success: true; data: T } => ({ + success: true as const, data, }); const err = ( error: T, - ): Result => ({ - success: false, + ): { success: false; error: T } => ({ + success: false as const, error, }); From b943dd136621610eeef303cfb005c02cbd32cdd3 Mon Sep 17 00:00:00 2001 From: bgrcs Date: Mon, 23 Feb 2026 12:40:27 +0000 Subject: [PATCH 13/14] feat: 1.0.0-beta.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df682f3..660766f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neverpanic", - "version": "1.0.0-beta.3", + "version": "1.0.0-beta.4", "repository": { "url": "https://github.com/bgrcs/neverpanic" }, From 827954e3d45831145cbc9495e09e5edd69171cba Mon Sep 17 00:00:00 2001 From: bgrcs Date: Mon, 23 Feb 2026 13:42:37 +0000 Subject: [PATCH 14/14] feat: 1.0.0-beta.5 --- index.test.ts | 47 +++++++---------------------------------------- index.ts | 35 ++++++++++++++++++++++------------- package.json | 2 +- 3 files changed, 30 insertions(+), 54 deletions(-) diff --git a/index.test.ts b/index.test.ts index 39a41c0..5f8646a 100644 --- a/index.test.ts +++ b/index.test.ts @@ -194,38 +194,20 @@ describe("resultsToResult", () => { if (result.success) { expectTypeOf(result).toMatchObjectType<{ success: true; - data: { - success: true; - data: "some data"; - error?: undefined; - }[]; + data: "some data"[]; }>(); } if (!result.success) { expectTypeOf(result).toMatchObjectType<{ success: false; - error: ( - | { - success: false; - error: "SOME_ERROR"; - data?: undefined; - } - | { - success: false; - error: "SOME_OTHER_ERROR"; - data?: undefined; - } - )[]; + error: ("SOME_ERROR" | "SOME_OTHER_ERROR")[]; }>(); expect(result.success).toBe(false); expect(result.error).toMatchObject([ - { success: false, error: "SOME_ERROR" as const }, - { - success: false, - error: "SOME_OTHER_ERROR" as const, - }, + "SOME_ERROR" as const, + "SOME_OTHER_ERROR" as const, ]); } }); @@ -248,28 +230,13 @@ describe("resultsToResult", () => { expectTypeOf(result).toMatchObjectType<{ success: true; - data: ( - | { - success: true; - data: "some data"; - } - | { - success: true; - data: "other data"; - } - )[]; + data: ("some data" | "other data")[]; }>(); expect(result.success).toBe(true); expect(result.data).toMatchObject([ - { - success: true, - data: "some data", - }, - { - success: true, - data: "other data", - }, + "some data" as const, + "other data" as const, ]); }); }); diff --git a/index.ts b/index.ts index fca6eb0..67e5756 100644 --- a/index.ts +++ b/index.ts @@ -5,6 +5,19 @@ export type Result = } | { success: false; error: E }; +type DataOf = R extends { + success: true; + data: infer D; +} + ? D + : never; +type ErrorOf = R extends { + success: false; + error: infer E; +} + ? E + : never; + /** * Create a typesafe instance of neverpanic. * @@ -146,16 +159,12 @@ export const createNeverpanic = < * * const result = n.resultsToResult(findUserResults) */ - const resultsToResult = < - T extends Result[], - TD extends Extract, - TE extends Extract, - >( - results: T, - ): Result => { - const errors = results.filter( - (result): result is TE => !result.success, - ); + const resultsToResult = ( + results: R, + ): Result[], ErrorOf[]> => { + const errors = results + .filter((result) => !result.success) + .map((result) => result.error as ErrorOf); if (errors.length) return { @@ -163,9 +172,9 @@ export const createNeverpanic = < error: errors, }; - const successes = results.filter( - (result): result is TD => result.success, - ); + const successes = results + .filter((result) => !!result.success) + .map((result) => result.data as DataOf); return { success: true, diff --git a/package.json b/package.json index 660766f..a95c18f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neverpanic", - "version": "1.0.0-beta.4", + "version": "1.0.0-beta.5", "repository": { "url": "https://github.com/bgrcs/neverpanic" },