From 2b741b10c000e905981cdd9cd789fb5c24dfc403 Mon Sep 17 00:00:00 2001 From: Kosui Iwasa <24590869+iwasa-kosui@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:47:07 +0900 Subject: [PATCH 1/6] feat: add Do / bind / let for Result and ResultAsync Add Do notation support for composing multiple dependent Result/ResultAsync operations without nested closures. - Result.Do() and ResultAsync.Do as entry points - bind: accumulate values from Result-returning functions - let: accumulate values from pure functions - Error types accumulate as union, consistent with andThen - ResultAsync.bind accepts both sync Result and ResultAsync Closes #679 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/result-async.ts | 45 +++++++++ src/result.ts | 56 +++++++++++ tests/index.test.ts | 205 +++++++++++++++++++++++++++++++++++++++ tests/typecheck-tests.ts | 104 ++++++++++++++++++++ 4 files changed, 410 insertions(+) diff --git a/src/result-async.ts b/src/result-async.ts index 20120d2b..51c1d1d5 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -86,6 +86,51 @@ export class ResultAsync implements PromiseLike> { ) as CombineResultsWithAllErrorsArrayAsync } + // eslint-disable-next-line @typescript-eslint/ban-types + static get Do(): ResultAsync<{}, never> { + return okAsync({}) + } + + bind( + name: N, + f: (t: T) => Result | ResultAsync, + ): ResultAsync { + return new ResultAsync( + this._promise.then(async (res) => { + if (res.isErr()) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new Err(res.error) as any + } + + const newRes = await f(res.value) + if (newRes.isErr()) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new Err(newRes.error) as any + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new Ok({ ...(res.value as any), [name as string]: newRes.value }) as any + }), + ) + } + + let( + name: N, + f: (t: T) => B, + ): ResultAsync { + return new ResultAsync( + this._promise.then(async (res) => { + if (res.isErr()) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new Err(res.error) as any + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new Ok({ ...(res.value as any), [name as string]: f(res.value) }) as any + }), + ) + } + map(f: (t: T) => A | Promise): ResultAsync { return new ResultAsync( this._promise.then(async (res: Result) => { diff --git a/src/result.ts b/src/result.ts index ad447caa..c49f0724 100644 --- a/src/result.ts +++ b/src/result.ts @@ -57,6 +57,10 @@ export namespace Result { ): CombineResultsWithAllErrorsArray { return combineResultListWithAllErrors(resultList) as CombineResultsWithAllErrorsArray } + // eslint-disable-next-line @typescript-eslint/ban-types + export function Do(): Result<{}, never> { + return ok({}) + } } export type Result = Ok | Err @@ -275,6 +279,16 @@ interface IResult { */ match(ok: (t: T) => A, err: (e: E) => B): A | B + bind( + name: N, + f: (t: T) => Result, + ): Result + + let( + name: N, + f: (t: T) => B, + ): Result + /** * @deprecated will be removed in 9.0.0. * @@ -367,6 +381,30 @@ export class Ok implements IResult { return ok(this.value) } + bind( + name: N, + f: (t: T) => Result, + ): Result + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bind(name: string, f: (t: T) => Result): any { + const result = f(this.value) + if (result.isErr()) { + return err(result.error) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ok({ ...(this.value as any), [name]: result.value }) + } + + let( + name: N, + f: (t: T) => B, + ): Result + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let(name: string, f: (t: T) => unknown): any { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ok({ ...(this.value as any), [name]: f(this.value) }) + } + asyncAndThen(f: (t: T) => ResultAsync): ResultAsync { return f(this.value) } @@ -471,6 +509,24 @@ export class Err implements IResult { return f(this.error) } + bind( + name: N, + f: (t: T) => Result, + ): Result + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bind(_name: string, _f: (t: T) => Result): any { + return err(this.error) + } + + let( + name: N, + f: (t: T) => B, + ): Result + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let(_name: string, _f: (t: T) => unknown): any { + return err(this.error) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars asyncAndThen(_f: (t: T) => ResultAsync): ResultAsync { return errAsync(this.error) diff --git a/tests/index.test.ts b/tests/index.test.ts index 9f089a9d..8bca51ad 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1295,3 +1295,208 @@ describe('ResultAsync', () => { }) }) }) + +describe('Result.Do / bind / let', () => { + it('Accumulates values with bind', () => { + const result = Result.Do() + .bind('x', () => ok(1)) + .bind('y', ({ x }) => ok(x + 1)) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({ x: 1, y: 2 }) + }) + + it('Short-circuits on first Err', () => { + const result = Result.Do() + .bind('x', () => ok(1)) + .bind('y', () => err('fail' as const)) + .bind('z', () => ok(3)) + + expect(result.isErr()).toBe(true) + expect(result._unsafeUnwrapErr()).toEqual('fail') + }) + + it('Does not call subsequent callbacks after Err', () => { + const spy = vitest.fn(() => ok(3)) + + Result.Do() + .bind('x', () => ok(1)) + .bind('y', () => err('fail')) + .bind('z', spy) + + expect(spy).not.toHaveBeenCalled() + }) + + it('Adds pure values with let', () => { + const result = Result.Do() + .bind('x', () => ok(10)) + .let('doubled', ({ x }) => x * 2) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({ x: 10, doubled: 20 }) + }) + + it('Combines bind and let', () => { + const result = Result.Do() + .bind('name', () => ok('Alice')) + .bind('age', () => ok(30)) + .let('greeting', ({ name, age }) => `${name} is ${age}`) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({ + name: 'Alice', + age: 30, + greeting: 'Alice is 30', + }) + }) + + it('let short-circuits when already Err', () => { + const result = Result.Do() + .bind('x', () => err('fail' as const)) + .let('y', () => 42) + + expect(result.isErr()).toBe(true) + expect(result._unsafeUnwrapErr()).toEqual('fail') + }) + + it('Accumulates different error types as union', () => { + const getUser = (): Result<{ name: string }, 'user-not-found'> => ok({ name: 'Alice' }) + const getRole = (_user: { name: string }): Result => ok('admin') + + const result = Result.Do() + .bind('user', () => getUser()) + .bind('role', ({ user }) => getRole(user)) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({ user: { name: 'Alice' }, role: 'admin' }) + }) + + it('Short-circuits mid-chain with correct error', () => { + const result = Result.Do() + .bind('a', () => ok(1)) + .bind('b', () => err('mid-error' as const)) + .let('c', ({ a }) => a * 10) + .bind('d', () => ok(4)) + + expect(result.isErr()).toBe(true) + expect(result._unsafeUnwrapErr()).toEqual('mid-error') + }) + + it('Do returns an empty object', () => { + const result = Result.Do() + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({}) + }) + + it('Chains map after bind', () => { + const result = Result.Do() + .bind('x', () => ok(2)) + .bind('y', () => ok(3)) + .map(({ x, y }) => x * y) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual(6) + }) + + it('Chains andThen after bind', () => { + const result = Result.Do() + .bind('x', () => ok(5)) + .andThen(({ x }) => ok(x * 2)) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual(10) + }) +}) + +describe('ResultAsync.Do / bind / let', () => { + it('Accumulates values with bind', async () => { + const result = await ResultAsync.Do + .bind('x', () => okAsync(1)) + .bind('y', ({ x }) => okAsync(x + 1)) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({ x: 1, y: 2 }) + }) + + it('Short-circuits on first Err', async () => { + const result = await ResultAsync.Do + .bind('x', () => okAsync(1)) + .bind('y', () => errAsync('fail' as const)) + .bind('z', () => okAsync(3)) + + expect(result.isErr()).toBe(true) + expect(result._unsafeUnwrapErr()).toEqual('fail') + }) + + it('Does not call subsequent callbacks after Err', async () => { + const spy = vitest.fn(() => okAsync(3)) + + await ResultAsync.Do + .bind('x', () => okAsync(1)) + .bind('y', () => errAsync('fail')) + .bind('z', spy) + + expect(spy).not.toHaveBeenCalled() + }) + + it('Adds pure values with let', async () => { + const result = await ResultAsync.Do + .bind('x', () => okAsync(10)) + .let('doubled', ({ x }) => x * 2) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({ x: 10, doubled: 20 }) + }) + + it('Combines bind and let', async () => { + const result = await ResultAsync.Do + .bind('name', () => okAsync('Bob')) + .bind('age', () => okAsync(25)) + .let('label', ({ name, age }) => `${name}:${age}`) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({ + name: 'Bob', + age: 25, + label: 'Bob:25', + }) + }) + + it('let short-circuits when already Err', async () => { + const result = await ResultAsync.Do + .bind('x', () => errAsync('fail' as const)) + .let('y', () => 42) + + expect(result.isErr()).toBe(true) + expect(result._unsafeUnwrapErr()).toEqual('fail') + }) + + it('Accepts sync Result in bind', async () => { + const result = await ResultAsync.Do + .bind('x', () => ok(1)) + .bind('y', ({ x }) => okAsync(x + 1)) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual({ x: 1, y: 2 }) + }) + + it('Chains map after bind', async () => { + const result = await ResultAsync.Do + .bind('x', () => okAsync(2)) + .bind('y', () => okAsync(3)) + .map(({ x, y }) => x * y) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual(6) + }) + + it('Chains andThen after bind', async () => { + const result = await ResultAsync.Do + .bind('x', () => okAsync(5)) + .andThen(({ x }) => okAsync(x * 2)) + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toEqual(10) + }) +}) diff --git a/tests/typecheck-tests.ts b/tests/typecheck-tests.ts index 55a3cd3d..f48591f8 100644 --- a/tests/typecheck-tests.ts +++ b/tests/typecheck-tests.ts @@ -2603,5 +2603,109 @@ declare function transpose< //#endregion +(function describe(_ = 'Result.Do / bind / let') { + (function it(_ = 'Infers accumulated type through bind chain') { + type Expectation = Result<{} & { readonly x: number } & { readonly y: string }, 'e1' | 'e2'> + + const result: Expectation = Result.Do() + .bind('x', () => ok(1)) + .bind('y', () => ok('hello')) + + Test.checks([ + Test.check(), + ]) + }); + + (function it(_ = 'Infers let type without adding to error union') { + type Expectation = Result<{} & { readonly x: number } & { readonly doubled: number }, 'e1'> + + const result: Expectation = Result.Do() + .bind('x', () => ok(1)) + .let('doubled', ({ x }) => x * 2) + + Test.checks([ + Test.check(), + ]) + }); + + (function it(_ = 'Callback receives accumulated context') { + Result.Do() + .bind('x', () => ok(1)) + .bind('y', ({ x }) => { + const _check: number = x + return ok(x + 1) + }) + .let('z', ({ x, y }) => { + const _checkX: number = x + const _checkY: number = y + return x + y + }) + }); + + (function it(_ = 'map receives accumulated context') { + type Expectation = Result + + const result: Expectation = Result.Do() + .bind('x', () => ok(1)) + .bind('y', () => ok(2)) + .map(({ x, y }) => x + y) + + Test.checks([ + Test.check(), + ]) + }); +}); + +(function describe(_ = 'ResultAsync.Do / bind / let') { + (function it(_ = 'Infers accumulated type through bind chain') { + type Expectation = ResultAsync<{} & { readonly x: number } & { readonly y: string }, 'e1' | 'e2'> + + const result: Expectation = ResultAsync.Do + .bind('x', () => okAsync(1)) + .bind('y', () => okAsync('hello')) + + Test.checks([ + Test.check(), + ]) + }); + + (function it(_ = 'Accepts sync Result in bind') { + type Expectation = ResultAsync<{} & { readonly x: number } & { readonly y: string }, 'e1' | 'e2'> + + const result: Expectation = ResultAsync.Do + .bind('x', () => ok(1)) + .bind('y', () => okAsync('hello')) + + Test.checks([ + Test.check(), + ]) + }); + + (function it(_ = 'Infers let type') { + type Expectation = ResultAsync<{} & { readonly x: number } & { readonly doubled: number }, 'e1'> + + const result: Expectation = ResultAsync.Do + .bind('x', () => okAsync(1)) + .let('doubled', ({ x }) => x * 2) + + Test.checks([ + Test.check(), + ]) + }); + + (function it(_ = 'map receives accumulated context') { + type Expectation = ResultAsync + + const result: Expectation = ResultAsync.Do + .bind('x', () => okAsync(1)) + .bind('y', () => okAsync(2)) + .map(({ x, y }) => x + y) + + Test.checks([ + Test.check(), + ]) + }); +}); + // create dummy values with a desired type const input = (): T => 123 as any From ae17b7aa5bf4bdaccabe0792350afdf67731e5c0 Mon Sep 17 00:00:00 2001 From: Kosui Iwasa <24590869+iwasa-kosui@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:54:42 +0900 Subject: [PATCH 2/6] chore: add changeset for Do / bind / let Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/add-do-bind-let.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add-do-bind-let.md diff --git a/.changeset/add-do-bind-let.md b/.changeset/add-do-bind-let.md new file mode 100644 index 00000000..36025a71 --- /dev/null +++ b/.changeset/add-do-bind-let.md @@ -0,0 +1,5 @@ +--- +"neverthrow": minor +--- + +Add Do / bind / let for Result and ResultAsync From acbace62ed57b2e5b457b4a4a053c3cf0e000c7c Mon Sep 17 00:00:00 2001 From: Kosui Iwasa <24590869+iwasa-kosui@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:56:39 +0900 Subject: [PATCH 3/6] refactor: replace as any with as unknown as in bind/let Co-Authored-By: Claude Opus 4.6 (1M context) --- src/result-async.ts | 17 +++++++---------- src/result.ts | 18 ++++++------------ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index 51c1d1d5..bfbe32c6 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -98,18 +98,16 @@ export class ResultAsync implements PromiseLike> { return new ResultAsync( this._promise.then(async (res) => { if (res.isErr()) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new Err(res.error) as any + return new Err(res.error) as unknown as Result } const newRes = await f(res.value) if (newRes.isErr()) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new Err(newRes.error) as any + return new Err(newRes.error) as unknown as Result } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new Ok({ ...(res.value as any), [name as string]: newRes.value }) as any + const merged = { ...(res.value as unknown as Record), [name as string]: newRes.value } + return new Ok(merged) as unknown as Result }), ) } @@ -121,12 +119,11 @@ export class ResultAsync implements PromiseLike> { return new ResultAsync( this._promise.then(async (res) => { if (res.isErr()) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new Err(res.error) as any + return new Err(res.error) as unknown as Result } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new Ok({ ...(res.value as any), [name as string]: f(res.value) }) as any + const merged = { ...(res.value as unknown as Record), [name as string]: f(res.value) } + return new Ok(merged) as unknown as Result }), ) } diff --git a/src/result.ts b/src/result.ts index c49f0724..ea791baf 100644 --- a/src/result.ts +++ b/src/result.ts @@ -385,24 +385,20 @@ export class Ok implements IResult { name: N, f: (t: T) => Result, ): Result - // eslint-disable-next-line @typescript-eslint/no-explicit-any - bind(name: string, f: (t: T) => Result): any { + bind(name: string, f: (t: T) => Result): Result { const result = f(this.value) if (result.isErr()) { return err(result.error) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ok({ ...(this.value as any), [name]: result.value }) + return ok({ ...(this.value as unknown as Record), [name]: result.value }) } let( name: N, f: (t: T) => B, ): Result - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let(name: string, f: (t: T) => unknown): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ok({ ...(this.value as any), [name]: f(this.value) }) + let(name: string, f: (t: T) => unknown): Result { + return ok({ ...(this.value as unknown as Record), [name]: f(this.value) }) } asyncAndThen(f: (t: T) => ResultAsync): ResultAsync { @@ -513,8 +509,7 @@ export class Err implements IResult { name: N, f: (t: T) => Result, ): Result - // eslint-disable-next-line @typescript-eslint/no-explicit-any - bind(_name: string, _f: (t: T) => Result): any { + bind(_name: string, _f: (t: T) => Result): Result { return err(this.error) } @@ -522,8 +517,7 @@ export class Err implements IResult { name: N, f: (t: T) => B, ): Result - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let(_name: string, _f: (t: T) => unknown): any { + let(_name: string, _f: (t: T) => unknown): Result { return err(this.error) } From 778dddda2891a11331d14302f046d7fbfe474175 Mon Sep 17 00:00:00 2001 From: Kosui Iwasa <24590869+iwasa-kosui@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:59:39 +0900 Subject: [PATCH 4/6] refactor: use implementation overloads to reduce type casts in ResultAsync Co-Authored-By: Claude Opus 4.6 (1M context) --- src/result-async.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index bfbe32c6..d89ae2a6 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -94,20 +94,20 @@ export class ResultAsync implements PromiseLike> { bind( name: N, f: (t: T) => Result | ResultAsync, - ): ResultAsync { + ): ResultAsync + bind(name: string, f: (t: T) => Result | ResultAsync): ResultAsync { return new ResultAsync( this._promise.then(async (res) => { if (res.isErr()) { - return new Err(res.error) as unknown as Result + return new Err(res.error) } const newRes = await f(res.value) if (newRes.isErr()) { - return new Err(newRes.error) as unknown as Result + return new Err(newRes.error) } - const merged = { ...(res.value as unknown as Record), [name as string]: newRes.value } - return new Ok(merged) as unknown as Result + return new Ok({ ...(res.value as unknown as Record), [name]: newRes.value }) }), ) } @@ -115,15 +115,15 @@ export class ResultAsync implements PromiseLike> { let( name: N, f: (t: T) => B, - ): ResultAsync { + ): ResultAsync + let(name: string, f: (t: T) => unknown): ResultAsync { return new ResultAsync( this._promise.then(async (res) => { if (res.isErr()) { - return new Err(res.error) as unknown as Result + return new Err(res.error) } - const merged = { ...(res.value as unknown as Record), [name as string]: f(res.value) } - return new Ok(merged) as unknown as Result + return new Ok({ ...(res.value as unknown as Record), [name]: f(res.value) }) }), ) } From 324f90767b87cbb93a31366c4c1551d1b7c1755f Mon Sep 17 00:00:00 2001 From: Kosui Iwasa <24590869+iwasa-kosui@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:00:23 +0900 Subject: [PATCH 5/6] style: apply prettier formatting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/result-async.ts | 20 +++++++++++++------- src/result.ts | 19 +++++-------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index d89ae2a6..c46ee148 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -95,7 +95,10 @@ export class ResultAsync implements PromiseLike> { name: N, f: (t: T) => Result | ResultAsync, ): ResultAsync - bind(name: string, f: (t: T) => Result | ResultAsync): ResultAsync { + bind( + name: string, + f: (t: T) => Result | ResultAsync, + ): ResultAsync { return new ResultAsync( this._promise.then(async (res) => { if (res.isErr()) { @@ -107,15 +110,15 @@ export class ResultAsync implements PromiseLike> { return new Err(newRes.error) } - return new Ok({ ...(res.value as unknown as Record), [name]: newRes.value }) + return new Ok({ + ...((res.value as unknown) as Record), + [name]: newRes.value, + }) }), ) } - let( - name: N, - f: (t: T) => B, - ): ResultAsync + let(name: N, f: (t: T) => B): ResultAsync let(name: string, f: (t: T) => unknown): ResultAsync { return new ResultAsync( this._promise.then(async (res) => { @@ -123,7 +126,10 @@ export class ResultAsync implements PromiseLike> { return new Err(res.error) } - return new Ok({ ...(res.value as unknown as Record), [name]: f(res.value) }) + return new Ok({ + ...((res.value as unknown) as Record), + [name]: f(res.value), + }) }), ) } diff --git a/src/result.ts b/src/result.ts index ea791baf..8f88b28a 100644 --- a/src/result.ts +++ b/src/result.ts @@ -284,10 +284,7 @@ interface IResult { f: (t: T) => Result, ): Result - let( - name: N, - f: (t: T) => B, - ): Result + let(name: N, f: (t: T) => B): Result /** * @deprecated will be removed in 9.0.0. @@ -390,15 +387,12 @@ export class Ok implements IResult { if (result.isErr()) { return err(result.error) } - return ok({ ...(this.value as unknown as Record), [name]: result.value }) + return ok({ ...((this.value as unknown) as Record), [name]: result.value }) } - let( - name: N, - f: (t: T) => B, - ): Result + let(name: N, f: (t: T) => B): Result let(name: string, f: (t: T) => unknown): Result { - return ok({ ...(this.value as unknown as Record), [name]: f(this.value) }) + return ok({ ...((this.value as unknown) as Record), [name]: f(this.value) }) } asyncAndThen(f: (t: T) => ResultAsync): ResultAsync { @@ -513,10 +507,7 @@ export class Err implements IResult { return err(this.error) } - let( - name: N, - f: (t: T) => B, - ): Result + let(name: N, f: (t: T) => B): Result let(_name: string, _f: (t: T) => unknown): Result { return err(this.error) } From cffd3638a84bb22c49e7d2728d0d7e33d2ee2442 Mon Sep 17 00:00:00 2001 From: Kosui Iwasa <24590869+iwasa-kosui@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:44:46 +0900 Subject: [PATCH 6/6] refactor: rewrite bind/let declaratively using andThen/map Co-Authored-By: Claude Opus 4.6 (1M context) --- src/result-async.ts | 37 +++++++++---------------------------- src/result.ts | 14 ++++++++------ 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index c46ee148..df1a0288 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -99,39 +99,20 @@ export class ResultAsync implements PromiseLike> { name: string, f: (t: T) => Result | ResultAsync, ): ResultAsync { - return new ResultAsync( - this._promise.then(async (res) => { - if (res.isErr()) { - return new Err(res.error) - } - - const newRes = await f(res.value) - if (newRes.isErr()) { - return new Err(newRes.error) - } - - return new Ok({ - ...((res.value as unknown) as Record), - [name]: newRes.value, - }) - }), + return this.andThen((val) => + f(val).map((newVal) => ({ + ...((val as unknown) as Record), + [name]: newVal, + })), ) } let(name: N, f: (t: T) => B): ResultAsync let(name: string, f: (t: T) => unknown): ResultAsync { - return new ResultAsync( - this._promise.then(async (res) => { - if (res.isErr()) { - return new Err(res.error) - } - - return new Ok({ - ...((res.value as unknown) as Record), - [name]: f(res.value), - }) - }), - ) + return this.map((val) => ({ + ...((val as unknown) as Record), + [name]: f(val), + })) } map(f: (t: T) => A | Promise): ResultAsync { diff --git a/src/result.ts b/src/result.ts index 8f88b28a..aebd4c95 100644 --- a/src/result.ts +++ b/src/result.ts @@ -383,16 +383,18 @@ export class Ok implements IResult { f: (t: T) => Result, ): Result bind(name: string, f: (t: T) => Result): Result { - const result = f(this.value) - if (result.isErr()) { - return err(result.error) - } - return ok({ ...((this.value as unknown) as Record), [name]: result.value }) + return f(this.value).map((val) => ({ + ...((this.value as unknown) as Record), + [name]: val, + })) } let(name: N, f: (t: T) => B): Result let(name: string, f: (t: T) => unknown): Result { - return ok({ ...((this.value as unknown) as Record), [name]: f(this.value) }) + return ok({ + ...((this.value as unknown) as Record), + [name]: f(this.value), + }) } asyncAndThen(f: (t: T) => ResultAsync): ResultAsync {