Skip to content

feat(core): add AsyncResult type#19

Open
AliiiBenn wants to merge 6 commits intomainfrom
feat/async-result-type
Open

feat(core): add AsyncResult type#19
AliiiBenn wants to merge 6 commits intomainfrom
feat/async-result-type

Conversation

@AliiiBenn
Copy link
Member

Summary

Add AsyncResult type for async railway-oriented programming - chaining asynchronous operations with proper error handling.

Changes

Types

  • AsyncResult<T, E> - Promise<AsyncOk | AsyncErr>
  • AsyncOk - Async success: { ok: true, value: T }
  • AsyncErr - Async error: { ok: false, error: E }

Constructors

okAsync(42)              // AsyncOk<number>
errAsync("error")       // AsyncErr<string>
fromPromise(fetchUser(1)) // From Promise

Chaining

okAsync(1)
  .map(x => x * 2)           // sync map
  .mapAsync(x => fetch(x))    // async map
  .flatMap(x => okAsync(x))   // sync flatMap
  .flatMapAsync(x => fetchOk(x)) // async flatMap

Parallel Operations

// Race - first to resolve wins
const winner = await race(okAsync(1), slowPromise)

// All - run in parallel, fail fast on error
const [users, posts] = await all(fetchUsers(), fetchPosts())

// Traverse - map async over array
const results = await traverse([1, 2, 3], id => fetchUser(id))

Coverage

100% test coverage. 32 tests.

🤖 Generated with Claude Code

AliiiBenn and others added 3 commits March 5, 2026 16:05
- Add Result<T, E> union type
- Add ok() and err() constructors
- Add isOk() and isErr() type guards with methods
- Add map(), flatMap(), mapErr() transformations
- Add getOrElse(), getOrCompute() extraction
- Add tap(), match() for side effects and pattern matching
- Add toNullable(), toUndefined() conversions
- 43 tests with 100% coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Try<T, E> union type
- Add attempt() for sync try/catch
- Add attemptAsync() for async try/catch
- Add isOk() and isErr() type guards
- Add map(), flatMap() transformations
- Add getOrElse(), getOrCompute() extraction
- Add tap(), match() for side effects
- Add toNullable(), toUndefined() conversions
- 27 tests with 100% coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add AsyncResult<T, E> type (Promise of Ok/Err)
- Add okAsync() and errAsync() constructors
- Add fromPromise() to convert Promise to AsyncResult
- Add isOk() and isErr() type guards
- Add map(), flatMap() sync transformations
- Add mapAsync(), flatMapAsync() async transformations
- Add getOrElse(), getOrCompute() extraction
- Add tap(), match() for side effects
- Add race(), all(), traverse() parallel utilities
- Add toNullable(), toUndefined() conversions
- 32 tests with 100% coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* @param value - The success value
* @returns Promise<AsyncOk<T>>
*/
export const okAsync = <T>(value: T): AsyncResult<T, never> =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The okAsync and errAsync functions don't freeze the returned objects, unlike the synchronous ok and err functions in result.ts which use Object.freeze(). This is an inconsistency that could lead to unexpected mutations.

const r = await result;
return isOk(r) ? ok(r.value) : err(r.error);
};

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mapErr function is missing from AsyncResult. The synchronous Result type has mapErr (result.ts:122-123) to transform errors, but AsyncResult doesn't have this utility. Consider adding mapErr and mapErrAsync functions.

* AsyncOk type - represents a successful async result
* @typeParam T - The type of the value
*/
export type AsyncOk<T> = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AsyncOk and AsyncErr types don't include isOk() and isErr() methods that exist on the synchronous Ok and Err types (result.ts:20-21, 30-32). This is a minor inconsistency in the API design.

* @param promise - The promise to convert
* @returns AsyncResult<T, Error>
*/
export const fromPromise = <T>(promise: Promise<T>): AsyncResult<T, Error> =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default error type is Error which differs from other error types that might be used. This is reasonable but users should be aware they can specify a more specific error type with fromPromise<number, CustomErrorType>.

AliiiBenn and others added 2 commits March 6, 2026 09:47
* @param value - The success value
* @returns Promise<AsyncOk<T>>
*/
export const okAsync = <T>(value: T): AsyncResult<T, never> =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync Result type uses Object.freeze() to make objects immutable (see result.ts:40-50 and result.ts:57-67), but the async versions (okAsync and errAsync in async-result.ts:41-56) don't freeze the returned objects. Consider adding Object.freeze() for consistency.

* @typeParam T - The type of the success value
* @typeParam E - The type of the error
*/
export type AsyncResult<T, E = Error> = Promise<AsyncResultInner<T, E>>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync Result type includes isOk() and isErr() methods on the Ok and Err types (see result.ts:20-21 and result.ts:31-32). The AsyncResult type doesn't include these methods. Consider adding them for API consistency.

* @param result - The AsyncResult to check
* @returns true if AsyncResult is AsyncErr<E>
*/
export const isErr = <T, E>(result: AsyncResultInner<T, E>): result is AsyncErr<E> =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync Result type has a mapErr function (result.ts:122-123) to transform errors. AsyncResult is missing a corresponding mapErr (both sync and async versions). Consider adding mapErr and mapErrAsync functions.

* @param results - The AsyncResults to race
* @returns Promise<T> - The value of the first resolved
*/
export const race = <T, E>(...results: Array<AsyncResult<T, E>>): Promise<T> =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The race function throws when encountering an error. This could be problematic in some use cases where you'd want to handle errors differently. Consider documenting this behavior clearly or providing a variant that returns AsyncResult<T, E> instead of throwing.

The getOrCompute function was returning T | Promise<U> inside
Promise.then(), which TypeScript couldn't properly type. Changed
to use async/await for proper type inference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* AsyncOk type - represents a successful async result
* @typeParam T - The type of the value
*/
export type AsyncOk<T> = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AsyncOk and AsyncErr types don't have isOk() and isErr() methods, unlike the sync Ok and Err types in result.ts. This is an inconsistency in the API. Consider adding these methods for consistency.

* @param value - The success value
* @returns Promise<AsyncOk<T>>
*/
export const okAsync = <T>(value: T): AsyncResult<T, never> =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync ok constructor (result.ts:40-50) uses Object.freeze() to make the result immutable, but the async okAsync constructor doesn't. Consider adding Object.freeze() for consistency.

* @param fn - The async mapping function
* @returns AsyncResult<U, E>
*/
export const mapAsync = async <T, E, U>(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync Result type has a mapErr function (result.ts:122-123) to transform errors, but AsyncResult doesn't have an equivalent. Consider adding mapErr and mapErrAsync functions for completeness.

});
});

describe("mapAsync", () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions 100% test coverage with 32 tests, but I count ~30 tests in this file. Missing edge case tests: (1) mapAsync when the async function throws, (2) flatMapAsync returning an error, (3) race when one result errors before another resolves.

* @param results - The AsyncResults to race
* @returns Promise<T> - The value of the first resolved
*/
export const race = <T, E>(...results: Array<AsyncResult<T, E>>): Promise<T> =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The race function behavior when mixing successes and errors may be non-deterministic. Consider documenting the exact behavior or adding a variant (e.g., raceOk) that only resolves on success and ignores errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant