From f21514e5ddfc060768bdb9e9a13babf06a29347f Mon Sep 17 00:00:00 2001 From: Hossein Pourdavar Date: Thu, 25 Jun 2026 18:49:42 +0330 Subject: [PATCH 1/2] feat(ddd): add Result.ok() void overload for command use cases Support no-value success via overloaded Result.ok(), default void type params on Ok/Result, and broaden flatMap error typing with tests. Co-authored-by: Cursor --- .../shared/domain/__tests__/result.spec.ts | 8 ++ .../shared/domain/__tests__/result.test-d.ts | 2 + packages/ddd/src/shared/domain/result.ts | 85 +++++++++---------- 3 files changed, 48 insertions(+), 47 deletions(-) diff --git a/packages/ddd/src/shared/domain/__tests__/result.spec.ts b/packages/ddd/src/shared/domain/__tests__/result.spec.ts index e473aa0..806cb34 100644 --- a/packages/ddd/src/shared/domain/__tests__/result.spec.ts +++ b/packages/ddd/src/shared/domain/__tests__/result.spec.ts @@ -25,6 +25,14 @@ describe('result', () => { expect(result.value).toBe(value); }); + it('creates a frozen ok result without a value', () => { + const result = Result.ok(); + + expect(result).toEqual({ value: undefined, kind: 'ok' }); + expect(Object.isFrozen(result)).toBe(true); + expect(Result.isOk(result)).toBe(true); + }); + it('creates a frozen err result with the given error', () => { const error = new InvalidValueError('invalid'); const result = Result.err(error); diff --git a/packages/ddd/src/shared/domain/__tests__/result.test-d.ts b/packages/ddd/src/shared/domain/__tests__/result.test-d.ts index 97e73b8..6ce1df5 100644 --- a/packages/ddd/src/shared/domain/__tests__/result.test-d.ts +++ b/packages/ddd/src/shared/domain/__tests__/result.test-d.ts @@ -12,10 +12,12 @@ class SampleAppError extends ApplicationError { } const okNumber = ResultNs.ok(42); +const okVoid = ResultNs.ok(); const errDomain = ResultNs.err(new InvalidStateError('bad')); const errApp = ResultNs.err(new SampleAppError()); expectType>(okNumber); +expectType>(okVoid); expectType>(errDomain); expectType>(errApp); diff --git a/packages/ddd/src/shared/domain/result.ts b/packages/ddd/src/shared/domain/result.ts index 81b4a01..aae8de4 100644 --- a/packages/ddd/src/shared/domain/result.ts +++ b/packages/ddd/src/shared/domain/result.ts @@ -5,7 +5,7 @@ export type { UseCaseError } from '../use-case.error'; /** * Successful outcome of an application use case. */ -export type Ok = Readonly<{ +export type Ok = Readonly<{ kind: 'ok'; value: T; }>; @@ -21,32 +21,30 @@ export type Err = Readonly<{ /** * Explicit success/failure for application-layer use cases. * - * Domain entities and value objects throw on invariant violations; - * application services return Result instead of throwing expected failures. + * Domain entities and value objects may throw on invariant violations. + * Application services should return Result for expected use-case failures. * - * @template T Success value type - * @template E Per-use-case error union (`ApplicationError`, `DomainError`, etc.) + * @template T Success value type. Defaults to void for command use cases. + * @template E Per-use-case error union. */ -export type Result = Err | Ok; +export type Result = + | Err + | Ok; function freeze>(result: R): R { return Object.freeze(result); } -/** - * Factory and combinators for {@link Result}. - * - * @example - * ```typescript - * function login(id: string): Result { - * if (!valid) return Result.err(new AuthInvalidCredentialsError(id)); - * return Result.ok(session); - * } - * - * return Result.flatMap(loadUser(id), (user) => issueSession(user)); - * ``` - */ +function ok(): Ok; +function ok(value: T): Ok; +function ok(value?: T): Ok { + return freeze({ kind: 'ok', value }); +} + export const Result = { + /** + * Pattern-matches a Result into a single output value. + */ match( result: Result, handlers: { @@ -54,44 +52,39 @@ export const Result = { err: (error: E) => U; }, ): U { - if (result.kind === 'err') { - return handlers.err(result.error); - } + return result.kind === 'err' + ? handlers.err(result.error) + : handlers.ok(result.value); + }, - return handlers.ok(result.value); + /** + * Chains another Result-producing operation. + */ + flatMap( + result: Result, + fn: (value: T) => Result, + ): Result { + return result.kind === 'err' ? result : fn(result.value); }, + /** + * Transforms the error value. + */ mapError( result: Result, fn: (error: E) => F, ): Result { - if (result.kind === 'ok') { - return result; - } - - return Result.err(fn(result.error)); - }, - - flatMap( - result: Result, - fn: (value: T) => Result, - ): Result { - if (result.kind === 'err') { - return result; - } - - return fn(result.value); + return result.kind === 'ok' ? result : Result.err(fn(result.error)); }, + /** + * Transforms the success value. + */ map( result: Result, fn: (value: T) => U, ): Result { - if (result.kind === 'err') { - return result; - } - - return Result.ok(fn(result.value)); + return result.kind === 'err' ? result : Result.ok(fn(result.value)); }, isErr(result: Result): result is Err { @@ -106,7 +99,5 @@ export const Result = { return freeze({ kind: 'err', error }); }, - ok(value: T): Ok { - return freeze({ kind: 'ok', value }); - }, + ok, } as const; From 2b3b6ac605a0e3789b39a043506ca0533234cdc4 Mon Sep 17 00:00:00 2001 From: Hossein Pourdavar Date: Thu, 25 Jun 2026 18:54:25 +0330 Subject: [PATCH 2/2] chore(changeset): add minor bump for Result.ok() void overload Co-authored-by: Cursor --- .changeset/result-ok-void-overload.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/result-ok-void-overload.md diff --git a/.changeset/result-ok-void-overload.md b/.changeset/result-ok-void-overload.md new file mode 100644 index 0000000..164e27c --- /dev/null +++ b/.changeset/result-ok-void-overload.md @@ -0,0 +1,11 @@ +--- +'@rineex/ddd': minor +--- + +Add `Result.ok()` void overload for command use cases that succeed without a +value. + +- `Result.ok()` returns `Ok`; `Result.ok(value)` returns `Ok` as before +- `Ok` and `Result` default type parameters to `void` and + `UseCaseError` +- `flatMap` widened to allow distinct error types in chained operations