diff --git a/README.md b/README.md index a221c24..c369823 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,21 @@ const posts = result.data posts.map((post) => console.log(post.title)); ``` +### `.map` + +```ts +const get = makeMightFail(axios.get); +const [ error, result ] = await get("/posts").map(post => post.title); + +if (error) { + // handle error + return; +} + +const titles = result.data +titles.map((post) => console.log(title)); +``` + ## Sync diff --git a/docs/content/contents.mdx b/docs/content/contents.mdx index 79caf18..65c2182 100644 --- a/docs/content/contents.mdx +++ b/docs/content/contents.mdx @@ -27,6 +27,10 @@ course: filePath: /getting-started/makeMightFailSync.mdx slug: /make-might-fail-sync type: documentation + - title: "`.map` Transformations" + filePath: /getting-started/map-transformations.mdx + slug: /map-transformations + type: documentation - title: "Either Type" filePath: /getting-started/either.mdx slug: /either diff --git a/docs/content/getting-started/getting-started.mdx b/docs/content/getting-started/getting-started.mdx index c455540..0a0edb0 100644 --- a/docs/content/getting-started/getting-started.mdx +++ b/docs/content/getting-started/getting-started.mdx @@ -2,7 +2,10 @@ * [❌ try, catch, finally](/try-catch-finally-is-bad) * [`mightFail`](/might-fail) +* [`makeMightFail`](/make-might-fail) * [`mightFailSync`](/might-fail-sync) +* [`makeMightFailSync`](/make-might-fail-sync) +* [`.map` Transformations](/map-transformations) * [Static Methods](/static-methods) * [Either Type](/either) * [Might & Fail](/might-and-fail) @@ -41,6 +44,32 @@ posts.map((post) => console.log(post.title)); The success case is now the only code that is **not** nested in another block. It's also at the very bottom of the function making it easy to find. +## Transform Results with `.map` + +You can transform successful results without repeatedly handling errors: + +```ts +// Transform results in a functional style +const [ error, postTitles ] = await mightFail(fetch("/posts")) + .map(response => response.json()) + .map(posts => posts.filter(post => post.published)) + .map(published => published.map(post => post.title)) + +if (error) { + // Handle any error from the entire chain + return +} + +// postTitles contains the transformed result +console.log(postTitles) // Array of published post titles +``` + +The `.map` method: +- Only executes if the previous step succeeded +- Catches transformation errors automatically +- Can be chained for complex transformations +- Works with all might-fail functions + - The sucess case is always at the bottom of the function. All of your error handling logic is next to where the error might occur. + The success case is always at the bottom of the function. All of your error handling logic is next to where the error might occur. diff --git a/docs/content/getting-started/makeMightFail.mdx b/docs/content/getting-started/makeMightFail.mdx index db6a1d0..8290f82 100644 --- a/docs/content/getting-started/makeMightFail.mdx +++ b/docs/content/getting-started/makeMightFail.mdx @@ -51,4 +51,86 @@ posts.map((post) => console.log(post.title)); +## Transforming Results with `.map` + +You can transform results directly on the returned promise: + + + +```ts +const get = makeMightFail(axios.get); +const [error, titles] = await get("/posts") + .map(response => response.data) + .map(posts => posts.map(post => post.title)) + +if (error) { + // handle error + return; +} + +console.log(titles); // Array of post titles +``` + + +```ts +const get = makeMightFail(axios.get); +const { error, result: titles } = await get("/posts") + .map(response => response.data) + .map(posts => posts.map(post => post.title)) + +if (error) { + // handle error + return; +} + +console.log(titles); // Array of post titles +``` + + +```ts +const get = makeMightFail(axios.get); +const [titles, error] = await get("/posts") + .map(response => response.data) + .map(posts => posts.map(post => post.title)) + +if (error) { + // handle error + return; +} + +console.log(titles); // Array of post titles +``` + + + +This is particularly useful for API responses where you want to extract and transform specific data: + +```ts +const fetchUser = makeMightFail(async (id: number) => { + const response = await fetch(`/users/${id}`) + return response.json() +}) + +const [error, userSummary] = await fetchUser(123) + .map(user => ({ + name: user.name, + email: user.email, + isActive: user.lastSeen > Date.now() - 86400000 // 24h + })) + .map(summary => ({ + ...summary, + displayName: summary.isActive ? `${summary.name} (Online)` : summary.name + })) + +if (error) { + console.error('Failed to fetch user:', error.message) + return +} + +console.log(userSummary) +``` + + +Learn more about transformations in the [`.map` Transformations](/map-transformations) guide. + diff --git a/docs/content/getting-started/map-transformations.mdx b/docs/content/getting-started/map-transformations.mdx new file mode 100644 index 0000000..3957e45 --- /dev/null +++ b/docs/content/getting-started/map-transformations.mdx @@ -0,0 +1,210 @@ +# `.map` Transformations + +The `.map` method allows you to transform successful results without having to handle errors repeatedly. It provides a functional programming style for chaining transformations while preserving error handling. + +## Basic Usage + +Instead of awaiting, checking for errors, and then transforming: + +```ts +// Without .map - more verbose +const [error1, userData] = await mightFail(fetchUser(123)) +if (error1) return + +const [error2, userName] = await mightFailSync(() => userData.name.toUpperCase()) +if (error2) return + +console.log(userName) +``` + +You can chain transformations directly: + +```ts +// With .map - concise and readable +const [error, userName] = await mightFail(fetchUser(123)) + .map(user => user.name.toUpperCase()) + +if (error) return + +console.log(userName) +``` + +## Available on All Functions + +The `.map` method is available on all might-fail functions: + + + +```ts +const [error, result] = await mightFail(promise) + .map(data => data.title.toUpperCase()) + +if (error) { + console.error('Error:', error.message) + return +} + +console.log(result) // Transformed data +``` + + + +```ts +const safeGetUser = makeMightFail(getUserById) + +const [error, result] = await safeGetUser(123) + .map(user => user.name) + +if (error) { + console.error('Error:', error.message) + return +} + +console.log(result) // User name +``` + + + +```ts +// mightFail.all +const [error, result] = await mightFail.all([ + fetchProject(id), + fetchProjectPositions(id) +]).map(([project, positions]) => ({ + ...project, + positionCount: positions.length +})) + +// mightFail.race +const [error, winner] = await mightFail.race([ + fetchFromServer1(), + fetchFromServer2() +]).map(data => data.result) + +// mightFail.any +const [error, success] = await mightFail.any([ + tryMethod1(), + tryMethod2(), + tryMethod3() +]).map(result => result.value) +``` + + + +## Chaining Multiple Transformations + +You can chain multiple `.map` calls for complex transformations: + +```ts +const [error, summary] = await mightFail(fetchUserPosts(userId)) + .map(posts => posts.filter(post => post.published)) + .map(published => published.map(post => post.title)) + .map(titles => titles.join(', ')) + .map(titleList => `Published posts: ${titleList}`) + +if (error) { + console.error('Failed to get post summary:', error.message) + return +} + +console.log(summary) // "Published posts: Post 1, Post 2, Post 3" +``` + +## Error Handling + +### Preserves Original Errors +If the original promise rejects, `.map` preserves the error: + +```ts +const [error, result] = await mightFail(Promise.reject(new Error("API failed"))) + .map(data => data.toUpperCase()) + +console.log(error?.message) // "API failed" +console.log(result) // undefined +``` + +### Catches Transformation Errors +If the transformation function throws, `.map` catches it: + +```ts +const [error, result] = await mightFail(Promise.resolve(null)) + .map(data => data.name.toUpperCase()) // Will throw on null + +console.log(error?.message) // "Cannot read properties of null" +console.log(result) // undefined +``` + +### Error Short-Circuiting +Once an error occurs, subsequent `.map` calls are skipped: + +```ts +const [error, result] = await mightFail(Promise.reject(new Error("API failed"))) + .map(data => data.step1()) // Skipped + .map(data => data.step2()) // Skipped + .map(data => data.step3()) // Skipped + +console.log(error?.message) // "API failed" +``` + +## Real-World Example + +Here's a practical example combining API calls and transformations: + +```ts +async function getUserDashboard(userId: string) { + const [error, dashboard] = await mightFail.all([ + fetchUser(userId), + fetchUserPosts(userId), + fetchUserStats(userId) + ]).map(([user, posts, stats]) => ({ + user: { + name: user.name, + email: user.email + }, + summary: { + totalPosts: posts.length, + publishedPosts: posts.filter(p => p.published).length, + totalViews: stats.views, + lastActive: user.lastSeen + } + })).map(data => ({ + ...data, + summary: { + ...data.summary, + description: `${data.user.name} has ${data.summary.publishedPosts} published posts with ${data.summary.totalViews} total views` + } + })) + + if (error) { + console.error('Failed to load dashboard:', error.message) + return null + } + + return dashboard +} +``` + +## Type Safety + +The `.map` method maintains full type safety. TypeScript will infer the correct types through the transformation chain: + +```ts +// TypeScript knows the types at each step +const [error, result] = await mightFail(Promise.resolve({ count: 5 })) + .map(data => data.count) // number + .map(count => count * 2) // number + .map(doubled => doubled.toString()) // string + +// result is inferred as string | undefined +// error is inferred as Error | undefined +``` + +## Destructuring Still Works + +The transformed result maintains the iterable `Either` interface: + +```ts +// Both work identically +const { error, result } = await mightFail(promise).map(transform) +const [error, result] = await mightFail(promise).map(transform) +``` \ No newline at end of file diff --git a/docs/content/getting-started/mightFail.mdx b/docs/content/getting-started/mightFail.mdx index 7c9c4ff..b553377 100644 --- a/docs/content/getting-started/mightFail.mdx +++ b/docs/content/getting-started/mightFail.mdx @@ -117,3 +117,68 @@ posts.map((post) => console.log(post.title)); +## Transforming Results with `.map` + +You can transform successful results using the `.map` method without having to handle errors repeatedly: + + + + +```ts +// Transform successful results +const [error, titles] = await mightFail(fetch("/posts")) + .map(response => response.json()) + .map(posts => posts.map(post => post.title)) + +if (error) { + // handle any error from the chain + return +} + +console.log(titles) // Array of post titles +``` + + + +```ts +// Transform successful results +const { error, result: titles } = await mightFail(fetch("/posts")) + .map(response => response.json()) + .map(posts => posts.map(post => post.title)) + +if (error) { + // handle any error from the chain + return +} + +console.log(titles) // Array of post titles +``` + + + +```ts +// Transform successful results +const [titles, error] = await mightFail(fetch("/posts")) + .map(response => response.json()) + .map(posts => posts.map(post => post.title)) + +if (error) { + // handle any error from the chain + return +} + +console.log(titles) // Array of post titles +``` + + + +The `.map` method: +- Only runs if the previous step succeeded +- Catches any errors thrown during transformation +- Can be chained multiple times +- Maintains full type safety + + +Learn more about transformations in the [`.map` Transformations](/map-transformations) guide. + + diff --git a/docs/content/more-things/static-methods.mdx b/docs/content/more-things/static-methods.mdx index d4334e8..6d58811 100644 --- a/docs/content/more-things/static-methods.mdx +++ b/docs/content/more-things/static-methods.mdx @@ -11,9 +11,11 @@ - `await mightFail.race([])` - `await mightFail.any([])` -These are identical to the static methods on `Promise` but they return an [`Either`](/either) type. +These are identical to the static methods on `Promise` but they return an [`Either`](/either) type and support [`.map` transformations](/map-transformations). -## Example +## Basic Examples + +### `mightFail.all` ```ts import { mightFail } from 'might-fail' @@ -31,3 +33,155 @@ if (error) { // result is an array of the results of the promises console.log(result.map((r) => r.message)) ``` + +### `mightFail.race` + +```ts +const [error, winner] = await mightFail.race([ + fetch('/api/server1/data'), + fetch('/api/server2/data'), + fetch('/api/server3/data') +]); + +if (error) { + console.error('All servers failed') + return +} + +console.log('Fastest server responded:', winner) +``` + +### `mightFail.any` + +```ts +const [error, success] = await mightFail.any([ + tryDatabase1(), + tryDatabase2(), + tryDatabase3() +]); + +if (error) { + console.error('All databases failed:', error.message) + return +} + +console.log('At least one database succeeded:', success) +``` + +## With `.map` Transformations + +All static methods support `.map` for transforming results: + +### Transform `mightFail.all` Results + +```ts +// Combine multiple API calls and transform the result +const [error, projectSummary] = await mightFail.all([ + fetchProject(projectId), + fetchProjectMembers(projectId), + fetchProjectStats(projectId) +]).map(([project, members, stats]) => ({ + name: project.name, + memberCount: members.length, + totalCommits: stats.commits, + isActive: stats.lastCommit > Date.now() - 86400000 // 24h +})).map(summary => ({ + ...summary, + description: `${summary.name} has ${summary.memberCount} members and ${summary.totalCommits} commits` +})) + +if (error) { + console.error('Failed to load project data:', error.message) + return +} + +console.log(projectSummary) +``` + +### Transform Race Winner + +```ts +const [error, fastestData] = await mightFail.race([ + fetchFromCDN(), + fetchFromMainServer(), + fetchFromBackupServer() +]).map(response => response.json()) + .map(data => ({ + ...data, + source: 'fastest-available', + fetchedAt: new Date().toISOString() + })) + +if (error) { + console.error('All sources failed') + return +} + +console.log('Data from fastest source:', fastestData) +``` + +### Process First Success + +```ts +const [error, processedResult] = await mightFail.any([ + tryExpensiveOperation(), + tryFallbackOperation(), + tryLastResortOperation() +]).map(result => ({ + value: result.data, + processedAt: Date.now(), + success: true +})) + +if (error) { + console.error('All operations failed') + return +} + +console.log('Successfully processed:', processedResult) +``` + +## Real-World Example + +Here's a practical example that combines multiple data sources: + +```ts +async function loadUserDashboard(userId: string) { + // Fetch user data and related information in parallel + const [error, dashboard] = await mightFail.all([ + fetchUser(userId), + fetchUserPosts(userId), + fetchUserNotifications(userId), + fetchUserSettings(userId) + ]).map(([user, posts, notifications, settings]) => ({ + user: { + id: user.id, + name: user.name, + avatar: user.avatar + }, + content: { + posts: posts.filter(p => p.published), + unreadNotifications: notifications.filter(n => !n.read).length + }, + preferences: settings + })).map(data => ({ + ...data, + summary: { + hasNewContent: data.content.unreadNotifications > 0, + postCount: data.content.posts.length, + welcomeMessage: `Welcome back, ${data.user.name}!` + } + })) + + if (error) { + console.error('Failed to load dashboard:', error.message) + return null + } + + return dashboard +} +``` + + +All static methods preserve the iterable nature of the `Either` type, so you can use both object destructuring `{ error, result }` and array destructuring `[error, result]`. + diff --git a/examples/mapExample.ts b/examples/mapExample.ts new file mode 100644 index 0000000..f569fb7 --- /dev/null +++ b/examples/mapExample.ts @@ -0,0 +1,41 @@ +import { makeMightFail } from "../src/makeMightFail" + +async function fetchPosts() { + return Promise.resolve({ + data: [ + { id: 1, title: "First Post", content: "Hello World" }, + { id: 2, title: "Second Post", content: "More content" } + ] + }) +} + +async function main() { + const get = makeMightFail(fetchPosts) + + // Example from README - extracting titles + const { error, result } = await get().map(response => + response.data.map(post => post.title) + ) + + if (error) { + console.error('Error:', error.message) + return + } + + console.log('Post titles:', result) + + // Chain multiple transformations + const { error: error2, result: result2 } = await get() + .map(response => response.data) + .map(posts => posts.length) + .map(count => `Found ${count} posts`) + + if (error2) { + console.error('Error:', error2.message) + return + } + + console.log('Count message:', result2) +} + +main() \ No newline at end of file diff --git a/examples/mightFailAllMapExample.ts b/examples/mightFailAllMapExample.ts new file mode 100644 index 0000000..2fbcef8 --- /dev/null +++ b/examples/mightFailAllMapExample.ts @@ -0,0 +1,87 @@ +import { mightFail } from "../src/index" + +// Simulate API calls +function getProject(id: string) { + return Promise.resolve({ + project: { id, name: "My Project", status: "active" } + }) +} + +function getProjectPositions(_id: string) { + return Promise.resolve({ + positions: [ + { id: 1, title: "Frontend Developer", department: "Engineering" }, + { id: 2, title: "Backend Developer", department: "Engineering" }, + { id: 3, title: "Product Manager", department: "Product" } + ] + }) +} + +async function main() { + const projectId = "project-123" + + console.log("=== Using Promise.all with mightFail and .map ===") + + // Original approach - manually wrapping Promise.all + const { error: error1, result: result1 } = await mightFail( + Promise.all([getProject(projectId), getProjectPositions(projectId)]) + ).map(([projectData, positionsData]) => ({ + ...projectData.project, + positions: positionsData.positions + })) + + if (error1) { + console.error('Error:', error1.message) + return + } + + console.log('Result with Promise.all:', result1) + + console.log("\n=== Using mightFail.all with .map ===") + + // New approach - using mightFail.all directly + const { error: error2, result: result2 } = await mightFail.all([ + getProject(projectId), + getProjectPositions(projectId) + ]).map(([projectData, positionsData]) => ({ + ...projectData.project, + positions: positionsData.positions + })) + + if (error2) { + console.error('Error:', error2.message) + return + } + + console.log('Result with mightFail.all:', result2) + + console.log("\n=== Chaining multiple transformations ===") + + // Chain multiple transformations + const { error: error3, result: result3 } = await mightFail.all([ + getProject(projectId), + getProjectPositions(projectId) + ]) + .map(([projectData, positionsData]) => ({ + project: projectData.project, + positions: positionsData.positions + })) + .map(data => ({ + ...data, + totalPositions: data.positions.length, + departments: [...new Set(data.positions.map(p => p.department))] + })) + .map(data => ({ + ...data, + summary: `${data.project.name} has ${data.totalPositions} positions across ${data.departments.length} departments` + })) + + if (error3) { + console.error('Error:', error3.message) + return + } + + console.log('Chained result:', result3) +} + +main() \ No newline at end of file diff --git a/examples/mightFailMapExample.ts b/examples/mightFailMapExample.ts new file mode 100644 index 0000000..e1c9954 --- /dev/null +++ b/examples/mightFailMapExample.ts @@ -0,0 +1,73 @@ +import { mightFail, makeMightFail } from "../src/index" + +async function fetchUser(id: number) { + return Promise.resolve({ + id, + name: "John Doe", + email: "john@example.com", + posts: [ + { id: 1, title: "First Post", views: 100 }, + { id: 2, title: "Second Post", views: 250 } + ] + }) +} + +async function main() { + console.log("=== Direct mightFail usage with .map ===") + + // Using mightFail directly with .map + const { error: error1, result: titles } = await mightFail(fetchUser(123)) + .map(user => user.posts.map(post => post.title)) + + if (error1) { + console.error('Error:', error1.message) + return + } + + console.log('Post titles:', titles) + + // Chain multiple transformations with mightFail + const { error: error2, result: summary } = await mightFail(fetchUser(456)) + .map(user => user.posts) + .map(posts => posts.reduce((total, post) => total + post.views, 0)) + .map(totalViews => `User has ${totalViews} total views`) + + if (error2) { + console.error('Error:', error2.message) + return + } + + console.log('Summary:', summary) + + console.log("\n=== makeMightFail usage with .map ===") + + // Using makeMightFail with .map + const safeGetUser = makeMightFail(fetchUser) + + const { error: error3, result: userEmail } = await safeGetUser(789) + .map(user => user.email.toUpperCase()) + + if (error3) { + console.error('Error:', error3.message) + return + } + + console.log('User email:', userEmail) + + // Error handling during transformation + console.log("\n=== Error handling in .map ===") + + const { error: error4, result: result4 } = await mightFail(fetchUser(999)) + .map(() => { + // Simulate an error during transformation + throw new Error("Something went wrong during processing") + }) + + if (error4) { + console.error('Caught transformation error:', error4.message) + } else { + console.log('Result:', result4) + } +} + +main() \ No newline at end of file diff --git a/src/makeMightFail.ts b/src/makeMightFail.ts index 51add4b..a6d0023 100644 --- a/src/makeMightFail.ts +++ b/src/makeMightFail.ts @@ -1,5 +1,6 @@ import { type Either } from "./Either.js" import { mightFail, mightFailSync } from "./mightFail.js" +import { MightFailPromise, createMightFailPromise } from "./utils/utils.type.js" /** * Utility type that unwraps a Promise type. If T is a Promise, it extracts the type the Promise resolves to, @@ -16,7 +17,7 @@ type UnwrapPromise = T extends Promise ? U : T * * @template T The function type that returns a Promise. - * @param {T} func - The async function to be wrapped. This function should return a Promise. + * @param func - The async function to be wrapped. This function should return a Promise. * @return {Function} A new function that, when called, returns a Promise that resolves with an Either object. * The Either object contains either a 'result' with the resolved value of the original Promise, or an 'error' if the Promise was rejected. * @@ -39,12 +40,16 @@ type UnwrapPromise = T extends Promise ? U : T * } * console.log('Fetched data:', result); */ + + export function makeMightFail Promise, E extends Error = Error>( func: T -): (...funcArgs: Parameters) => Promise>, E>> { - return async (...args: Parameters) => { +): (...funcArgs: Parameters) => MightFailPromise>, E> { + return (...args: Parameters) => { const promise = func(...args) - return mightFail(promise) + const mightFailPromise = mightFail(promise) as Promise>, E>> + + return createMightFailPromise>, E>(mightFailPromise) } } diff --git a/src/utils/createEither.ts b/src/utils/createEither.ts index 4ac05c1..e90c7d1 100644 --- a/src/utils/createEither.ts +++ b/src/utils/createEither.ts @@ -1,6 +1,6 @@ -import { AnyEither, EitherMode } from "./utils.type.js" -import type { Either as StandardEither } from "../Either.js" -import type { Either as GoEither } from "../go/Either.js" +import { AnyEither, EitherMode } from "./utils.type" +import type { Either as StandardEither } from "../Either" +import type { Either as GoEither } from "../go/Either" // This is not how we intended the tuple feature to work but this is the only way we could currently get TypeScript to play nice // this really should just be an interator on the either object, but it's much more complicated because of TS. diff --git a/src/utils/mightFailFunction.ts b/src/utils/mightFailFunction.ts index 8a65795..7a4131f 100644 --- a/src/utils/mightFailFunction.ts +++ b/src/utils/mightFailFunction.ts @@ -1,16 +1,19 @@ -import { type Either } from "../Either.js" import { handleError } from "./errors.js" import { createEither } from "./createEither.js" -import { MightFailFunction } from "./utils.type.js" +import { MightFailFunction, createMightFailPromise } from "./utils.type.js" -export const mightFailFunction: MightFailFunction<"standard"> = async function ( +export const mightFailFunction: MightFailFunction<"standard"> = function ( promise: T -): Promise, E>> { - try { - const result = await promise - return createEither, E>({ result, error: undefined }) - } catch (err) { - const error = handleError(err) - return createEither, E>({ error, result: undefined }) - } +) { + const basePromise = (async () => { + try { + const result = await promise + return createEither, E>({ result, error: undefined }) + } catch (err) { + const error = handleError(err) + return createEither, E>({ error, result: undefined }) + } + })() + + return createMightFailPromise(basePromise) } diff --git a/src/utils/utils.type.ts b/src/utils/utils.type.ts index a3c5c23..b1c92fe 100644 --- a/src/utils/utils.type.ts +++ b/src/utils/utils.type.ts @@ -1,5 +1,33 @@ import type { Either as StandardEither } from "../Either.js" import type { Either as GoEither } from "../go/Either.js" +import { createEither } from "./createEither.js" + +export interface MightFailPromise extends Promise> { + map(fn: (value: T) => U): MightFailPromise +} + +export function createMightFailPromise( + basePromise: Promise> +): MightFailPromise { + return Object.assign(basePromise, { + map(fn: (value: T) => U): MightFailPromise { + const mappedPromise = basePromise.then(either => { + if (either.error) { + return createEither({ error: either.error, result: undefined }) as StandardEither + } + try { + const mappedValue = fn(either.result!) + return createEither({ error: undefined, result: mappedValue }) as StandardEither + } catch (err) { + const error = err instanceof Error ? err as E : new Error(String(err)) as E + return createEither({ error, result: undefined }) as StandardEither + } + }) + + return createMightFailPromise(mappedPromise) + } + }) as MightFailPromise +} export type EitherMode = "standard" | "go" | "any" @@ -7,13 +35,11 @@ export type AnyEither = StandardEither | GoEit export type MightFailFunction = ( promise: T -) => Promise< - TEitherMode extends "standard" - ? StandardEither, E> +) => TEitherMode extends "standard" + ? MightFailPromise, E> : TEitherMode extends "go" - ? GoEither, E> - : AnyEither, E> -> + ? Promise, E>> + : MightFailPromise, E> | Promise, E>> export type PromiseFulfilledResult = { status: "fulfilled" @@ -39,13 +65,11 @@ export interface PromiseStaticMethods { */ all( values: T - ): Promise< - TEitherMode extends "standard" - ? Awaited }>> + ): TEitherMode extends "standard" + ? MightFailPromise<{ -readonly [P in keyof T]: Awaited }> : TEitherMode extends "go" - ? Awaited }>> - : Awaited }>> - > + ? Promise }>> + : MightFailPromise<{ -readonly [P in keyof T]: Awaited }> | Promise }>> /** * (From lib.es2025.iterable.d.ts) @@ -56,13 +80,11 @@ export interface PromiseStaticMethods { */ all( values: Iterable> - ): Promise< - TEitherMode extends "standard" - ? Awaited> + ): TEitherMode extends "standard" + ? MightFailPromise : TEitherMode extends "go" - ? Awaited> - : Awaited> - > + ? Promise> + : MightFailPromise | Promise> /** * Wraps a Promise.race call in a mightFail function. @@ -73,13 +95,11 @@ export interface PromiseStaticMethods { */ race( values: Iterable> - ): Promise< - TEitherMode extends "standard" - ? Awaited> + ): TEitherMode extends "standard" + ? MightFailPromise : TEitherMode extends "go" - ? Awaited> - : Awaited> - > + ? Promise> + : MightFailPromise | Promise> /** * Wraps a Promise.race call in a mightFail function. @@ -89,13 +109,11 @@ export interface PromiseStaticMethods { */ race( values: T - ): Promise< - TEitherMode extends "standard" - ? Awaited> + ): TEitherMode extends "standard" + ? MightFailPromise : TEitherMode extends "go" - ? Awaited> - : Awaited> - > + ? Promise> + : MightFailPromise | Promise> /** * Wraps a Promise.any call in a mightFail function. @@ -106,13 +124,11 @@ export interface PromiseStaticMethods { */ any( values: T - ): Promise< - TEitherMode extends "standard" - ? StandardEither, AggregateError> + ): TEitherMode extends "standard" + ? MightFailPromise : TEitherMode extends "go" - ? GoEither, AggregateError> - : AnyEither, AggregateError> - > + ? Promise> + : MightFailPromise | Promise> /** * Wraps a Promise.any call in a mightFail function. @@ -123,11 +139,9 @@ export interface PromiseStaticMethods { */ any( values: Iterable> - ): Promise< - TEitherMode extends "standard" - ? StandardEither, AggregateError> + ): TEitherMode extends "standard" + ? MightFailPromise : TEitherMode extends "go" - ? GoEither, AggregateError> - : AnyEither, AggregateError> - > + ? Promise> + : MightFailPromise | Promise> } diff --git a/test/eitherIterability.test.ts b/test/eitherIterability.test.ts new file mode 100644 index 0000000..ec5bf8d --- /dev/null +++ b/test/eitherIterability.test.ts @@ -0,0 +1,82 @@ +import { expect, test, describe } from "vitest" +import { mightFail, makeMightFail } from "../src/index" + +describe("Either iterability with .map", () => { + test("mightFail result is destructurable", async () => { + const [error, result] = await mightFail(Promise.resolve("test")) + expect(error).toBe(undefined) + expect(result).toBe("test") + }) + + test("mightFail with .map result is destructurable", async () => { + const [error, result] = await mightFail(Promise.resolve("test")).map(x => x.toUpperCase()) + expect(error).toBe(undefined) + expect(result).toBe("TEST") + }) + + test("makeMightFail result is destructurable", async () => { + const safeResolve = makeMightFail((value: string) => Promise.resolve(value)) + const [error, result] = await safeResolve("test") + expect(error).toBe(undefined) + expect(result).toBe("test") + }) + + test("makeMightFail with .map result is destructurable", async () => { + const safeResolve = makeMightFail((value: string) => Promise.resolve(value)) + const [error, result] = await safeResolve("test").map(x => x.toUpperCase()) + expect(error).toBe(undefined) + expect(result).toBe("TEST") + }) + + test("mightFail.all result is destructurable", async () => { + const [error, result] = await mightFail.all([Promise.resolve("a"), Promise.resolve("b")]) + expect(error).toBe(undefined) + expect(result).toEqual(["a", "b"]) + }) + + test("mightFail.all with .map result is destructurable", async () => { + const [error, result] = await mightFail.all([Promise.resolve("a"), Promise.resolve("b")]) + .map(([first, second]) => first + second) + expect(error).toBe(undefined) + expect(result).toBe("ab") + }) + + test("error cases are destructurable", async () => { + const [error, result] = await mightFail(Promise.reject(new Error("test error"))) + expect(result).toBe(undefined) + expect(error?.message).toBe("test error") + }) + + test("error cases with .map are destructurable", async () => { + const [error, result] = await mightFail(Promise.reject(new Error("test error"))) + .map((x: any) => x.toUpperCase()) + expect(result).toBe(undefined) + expect(error?.message).toBe("test error") + }) + + test("Either supports for...of iteration", async () => { + const either = await mightFail(Promise.resolve("test")).map(x => x.toUpperCase()) + const items: any[] = [] + for (const item of either) { + items.push(item) + } + expect(items).toEqual([undefined, "TEST"]) + }) + + test("Either supports array methods like forEach", async () => { + const either = await mightFail(Promise.resolve("test")).map(x => x.toUpperCase()) + const items: any[] = [] + either.forEach((item: any) => items.push(item)) + expect(items).toEqual([undefined, "TEST"]) + }) + + test("chained .map preserves iterability", async () => { + const [error, result] = await mightFail(Promise.resolve({ count: 5 })) + .map(data => data.count * 2) + .map(doubled => doubled + 1) + .map(final => final.toString()) + + expect(error).toBe(undefined) + expect(result).toBe("11") + }) +}) \ No newline at end of file diff --git a/test/makeMightFail.test.ts b/test/makeMightFail.test.ts index 5089fca..24fec14 100644 --- a/test/makeMightFail.test.ts +++ b/test/makeMightFail.test.ts @@ -24,3 +24,40 @@ test("fail without error returns an error", async () => { expect(result).toBe(undefined) expect(error?.message).toBeTruthy() }) + +test("map transforms successful result", async () => { + const resolve = (value: { count: number }) => Promise.resolve(value) + const func = makeMightFail(resolve) + const { error, result } = await func({ count: 5 }).map(data => data.count * 2) + expect(error).toBe(undefined) + expect(result).toBe(10) +}) + +test("map preserves error on failed result", async () => { + const reject = (error: Error) => Promise.reject(error) + const func = makeMightFail(reject) + const { error, result } = await func(new Error("original error")).map(data => data * 2) + expect(result).toBe(undefined) + expect(error?.message).toBe("original error") +}) + +test("map catches errors during transformation", async () => { + const resolve = (value: { count: number }) => Promise.resolve(value) + const func = makeMightFail(resolve) + const { error, result } = await func({ count: 5 }).map(() => { + throw new Error("transformation error") + }) + expect(result).toBe(undefined) + expect(error?.message).toBe("transformation error") +}) + +test("map can be chained multiple times", async () => { + const resolve = (value: { count: number }) => Promise.resolve(value) + const func = makeMightFail(resolve) + const { error, result } = await func({ count: 5 }) + .map(data => data.count * 2) + .map(doubled => doubled + 1) + .map(result => result.toString()) + expect(error).toBe(undefined) + expect(result).toBe("11") +}) diff --git a/test/mightFail.test.ts b/test/mightFail.test.ts index b61a16e..d0c3152 100644 --- a/test/mightFail.test.ts +++ b/test/mightFail.test.ts @@ -136,4 +136,35 @@ describe("Either factories (Might & Fail)", () => { }) }) }) + + describe("map functionality", () => { + test("map transforms successful result", async () => { + const { error, result } = await mightFail(Promise.resolve({ count: 5 })).map(data => data.count * 2) + expect(error).toBe(undefined) + expect(result).toBe(10) + }) + + test("map preserves error on failed result", async () => { + const { error, result } = await mightFail(Promise.reject(new Error("original error"))).map(data => data * 2) + expect(result).toBe(undefined) + expect(error?.message).toBe("original error") + }) + + test("map catches errors during transformation", async () => { + const { error, result } = await mightFail(Promise.resolve({ count: 5 })).map(() => { + throw new Error("transformation error") + }) + expect(result).toBe(undefined) + expect(error?.message).toBe("transformation error") + }) + + test("map can be chained multiple times", async () => { + const { error, result } = await mightFail(Promise.resolve({ count: 5 })) + .map(data => data.count * 2) + .map(doubled => doubled + 1) + .map(result => result.toString()) + expect(error).toBe(undefined) + expect(result).toBe("11") + }) + }) }) diff --git a/test/mightFailStaticMethodsMap.test.ts b/test/mightFailStaticMethodsMap.test.ts new file mode 100644 index 0000000..ce4a1a1 --- /dev/null +++ b/test/mightFailStaticMethodsMap.test.ts @@ -0,0 +1,60 @@ +import { expect, test, describe } from "vitest" +import { mightFail } from "../src/index" + +describe("mightFail static methods with .map", () => { + test("mightFail.all supports .map", async () => { + const promises = [ + Promise.resolve({ id: 1, name: "John" }), + Promise.resolve({ id: 2, name: "Jane" }) + ] + + const { error, result } = await mightFail.all(promises) + .map(users => users.map(user => user.name)) + + expect(error).toBe(undefined) + expect(result).toEqual(["John", "Jane"]) + }) + + test("mightFail.all preserves error with .map", async () => { + const promises = [ + Promise.resolve({ id: 1, name: "John" }), + Promise.reject(new Error("API error")) + ] + + const { error, result } = await mightFail.all(promises) + .map(users => users.map(user => user.name)) + + expect(result).toBe(undefined) + expect(error?.message).toBe("API error") + }) + + test("mightFail.all catches transformation errors", async () => { + const promises = [ + Promise.resolve({ id: 1, name: "John" }), + Promise.resolve({ id: 2, name: "Jane" }) + ] + + const { error, result } = await mightFail.all(promises) + .map(() => { + throw new Error("transformation error") + }) + + expect(result).toBe(undefined) + expect(error?.message).toBe("transformation error") + }) + + test("mightFail.all supports chained .map", async () => { + const promises = [ + Promise.resolve({ id: 1, score: 85 }), + Promise.resolve({ id: 2, score: 92 }) + ] + + const { error, result } = await mightFail.all(promises) + .map(users => users.map(user => user.score)) + .map(scores => scores.reduce((sum, score) => sum + score, 0)) + .map(total => `Total score: ${total}`) + + expect(error).toBe(undefined) + expect(result).toBe("Total score: 177") + }) +}) \ No newline at end of file