From d82a9cd182a4252d2fda77c79dbd1c08425e6a99 Mon Sep 17 00:00:00 2001 From: martyy-code Date: Fri, 29 May 2026 10:01:00 +0200 Subject: [PATCH 1/3] feat: implement raise() function Implement raise() function that throws ErrorInstance errors: - Returns never type for type safety - Throws the provided error instance - Works with native throw syntax - Exported from main module Files: - src/error/raise.ts - raise() implementation - tests/raise.test.ts - 9 unit tests Co-Authored-By: Claude Opus 4.7 --- .../agent-memory/typescript-expert/MEMORY.md | 7 + .../principal-level-error-system.md | 154 ++++++++++++++++++ packages/errors/src/error/raise.ts | 44 +++++ packages/errors/src/index.ts | 3 + packages/errors/tests/raise.test.ts | 107 ++++++++++++ 5 files changed, 315 insertions(+) create mode 100644 .claude/agent-memory/typescript-expert/MEMORY.md create mode 100644 .claude/agent-memory/typescript-expert/principal-level-error-system.md create mode 100644 packages/errors/src/error/raise.ts create mode 100644 packages/errors/tests/raise.test.ts diff --git a/.claude/agent-memory/typescript-expert/MEMORY.md b/.claude/agent-memory/typescript-expert/MEMORY.md new file mode 100644 index 0000000..9956faa --- /dev/null +++ b/.claude/agent-memory/typescript-expert/MEMORY.md @@ -0,0 +1,7 @@ +# TypeScript Expert Memory Index + +## Reference Documents + +- [record-string-unknown-pattern](record-string-unknown-pattern.md) — Senior to Principal TypeScript patterns +- [stack-capture-patterns](stack-capture-patterns.md) — Stack trace handling patterns +- [principal-level-error-system](principal-level-error-system.md) — Error factory system design patterns diff --git a/.claude/agent-memory/typescript-expert/principal-level-error-system.md b/.claude/agent-memory/typescript-expert/principal-level-error-system.md new file mode 100644 index 0000000..e6ae2de --- /dev/null +++ b/.claude/agent-memory/typescript-expert/principal-level-error-system.md @@ -0,0 +1,154 @@ +--- +name: principal-level-error-system +description: Principal Level patterns for error factory - type inference, runtime validation, polymorphism, branding +type: reference +--- + +# Principal Level: Error Factory System + +## 1. Automatic Type Inference from Schema + +Automatically extract output type from Standard Schema. + +```typescript +export const error = < + const TSchema extends StandardSchemaV1 | undefined = undefined, + TData = TSchema extends StandardSchemaV1 + ? StandardSchemaV1.InferOutput + : Record +>(config: { + name: string; + fields?: TSchema; + inherits?: ErrorFactory | ErrorFactory[]; + message?: string; +}): ErrorFactory => { ... } + +// Usage - types inferred automatically +const ValidationError = error({ + name: 'ValidationError', + fields: z.object({ field: z.string() }), +}); + +// err.fields.field is automatically typed as string +``` + +### Why This Matters + +- No manual generic needed +- Schema drives the entire type system +- Compile-time validation of required fields + +--- + +## 2. Runtime Contract Enforcement + +Always validate input against schema before creating instance. + +```typescript +const ErrorFactoryInstance = (input?: Partial): ErrorInstance => { + if (fields) { + const result = fields['~standard'].validate(input ?? {}); + + if (result instanceof Promise) { + // Note: async validation not supported in sync factory + } + + if (result.issues) { + // Throw if provided fields don't match schema + // Ensures ErrorInstance never contains invalid data + } + } + // ... rest +}; +``` + +### Why This Matters + +- `ErrorInstance` is always valid +- Catch errors early +- Schema is a true contract + +--- + +## 3. Polymorphism: The `is` Utility + +Check inheritance relationships across the chain. + +```typescript +export const is = (err: unknown, factory: ErrorFactory): boolean => { + if (!err || typeof err !== 'object' || !('_factory' in err)) { + return false; + } + + let current: ErrorFactory | ErrorFactory[] | undefined = + (err as ErrorInstance)._factory; + + const check = (f: ErrorFactory): boolean => { + if (f === factory) return true; + if (Array.isArray(f.inherits)) return f.inherits.some(check); + if (f.inherits) return check(f.inherits); + return false; + }; + + return check(current as ErrorFactory); +}; + +// Usage +const AppError = error({ name: 'AppError' }); +const ValidationError = error({ name: 'ValidationError', inherits: AppError }); + +const err = createValidationError(); +is(err, AppError); // ✅ true +is(err, ValidationError); // ✅ true +is(err, NetworkError); // ✅ false +``` + +### Why This Matters + +- `instanceof` doesn't work with plain objects +- Inheritance is functional, not just metadata +- Type-safe error checking + +--- + +## 4. Nominal Typing via Branding + +Prevent structural type collisions. + +```typescript +export type ErrorInstance< + TFields extends Record, + Name extends string +> = { + readonly __brand: Name; + name: Name; + fields: TFields; + // ... other properties +}; + +// Usage - errors are nominally typed +const ValidationError = error({ name: 'ValidationError' }); +const NetworkError = error({ name: 'NetworkError' }); + +// These are different types even with same shape +declare function processValidation(err: ErrorInstance<{}, 'ValidationError'>); +declare function processNetwork(err: ErrorInstance<{}, 'NetworkError'>); + +processValidation(NetworkError()); // ❌ Type error +``` + +### Why This Matters + +- TypeScript uses structural typing by default +- Branding creates nominal typing +- Prevents passing wrong error type to functions + +--- + +## Summary Table + +| Level | Focus | Key Feature | +|:---|:---|:---| +| Senior | Clean utilities | Regex, JSDoc, constants | +| Staff | Abstraction | StandardSchemaV1, inherits | +| Principal | Integrity | Auto-inference, runtime validation, is(), branding | diff --git a/packages/errors/src/error/raise.ts b/packages/errors/src/error/raise.ts new file mode 100644 index 0000000..a4aaa6d --- /dev/null +++ b/packages/errors/src/error/raise.ts @@ -0,0 +1,44 @@ +/** + * Error raising utilities. + */ + +import type { ErrorInstance } from './types.js'; + +/** + * Throws an ErrorInstance. + * + * This is the primary mechanism for throwing errors in @deessejs/errors. + * The library also supports native `throw` syntax for compatibility. + * + * @param error - An error created by an error factory + * @returns never - This function always throws + * + * @example + * ```typescript + * import { error, raise } from '@deessejs/errors'; + * + * const ValidationError = error({ + * name: 'ValidationError', + * fields: z.object({ field: z.string() }), + * }); + * + * raise(ValidationError({ field: 'email' })); + * ``` + * + * @example + * ```typescript + * // Also works with native throw + * throw ValidationError({ field: 'email' }); + * ``` + * + * @example + * ```typescript + * // Method chaining before raising + * raise(AppError().from(err).addNote('Context here')); + * ``` + */ +const raise = ( error: ErrorInstance ): never => { + throw error; +}; + +export { raise }; diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index 19966dc..9215a81 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -14,3 +14,6 @@ export type { // Error factory function export { error } from './error/error.js'; + +// Error raising function +export { raise } from './error/raise.js'; diff --git a/packages/errors/tests/raise.test.ts b/packages/errors/tests/raise.test.ts new file mode 100644 index 0000000..9caa4a7 --- /dev/null +++ b/packages/errors/tests/raise.test.ts @@ -0,0 +1,107 @@ +/** + * Unit tests for the raise() function. + */ + +import { describe, it, expect } from 'vitest'; +import { error } from '../src/error/error.js'; +import { raise } from '../src/error/raise.js'; +import type { ErrorInstance } from '../src/error/types.js'; + +describe( 'raise() function', () => { + describe( 'basic usage', () => { + it( 'should throw the error instance', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + expect( () => raise( instance ) ).toThrow( instance ); + } ); + + it( 'should have never return type (compile-time verification)', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + // TypeScript should infer that this function never returns + // The variable assignment itself should fail at compile time if return type is wrong + const throwFn: () => never = () => raise( instance ); + + // If we get here, TypeScript accepted the never return type + expect( typeof throwFn ).toBe( 'function' ); + } ); + + it( 'should preserve error properties on thrown error', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + try { + raise( instance ); + } catch ( err ) { + const caught = err as ErrorInstance; + expect( caught.name ).toBe( 'TestError' ); + expect( caught.message ).toBe( 'TestError' ); + expect( caught.stack ).toBeDefined(); + } + } ); + } ); + + describe( 'with native throw', () => { + it( 'should work with native throw syntax', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + expect( () => { + throw instance; + } ).toThrow( instance ); + } ); + } ); + + describe( 'with fields', () => { + it( 'should throw error with fields', () => { + const ValidationError = error<{ field: string }>( { + name: 'ValidationError', + message: 'Field "{field}" is invalid', + } ); + const instance = ValidationError( { field: 'email' } ); + + try { + raise( instance ); + } catch ( err ) { + const caught = err as ErrorInstance<{ field: string }>; + expect( caught.name ).toBe( 'ValidationError' ); + expect( caught.message ).toBe( 'Field "email" is invalid' ); + expect( caught.fields.field ).toBe( 'email' ); + } + } ); + } ); + + describe( 'chaining support', () => { + it( 'should work when chained from error factory', () => { + const TestError = error( { name: 'TestError' } ); + + expect( () => raise( TestError() ) ).toThrow(); + } ); + + it( 'should throw after .from() is implemented', () => { + // Note: .from() will be implemented in Task 05 + // For now, this test verifies raise() accepts ErrorInstance + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + expect( () => raise( instance ) ).toThrow(); + } ); + } ); + + describe( 'module export', () => { + it( 'should be exported from the module', () => { + expect( typeof raise ).toBe( 'function' ); + } ); + + it( 'should have correct function signature', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + // Type check: raise should accept ErrorInstance and return never + const fn: ( error: ErrorInstance ) => never = raise; + expect( fn ).toBe( raise ); + } ); + } ); +} ); From 6a66b4e8e0887525593a543e16981a755008008a Mon Sep 17 00:00:00 2001 From: martyy-code Date: Fri, 29 May 2026 10:04:20 +0200 Subject: [PATCH 2/3] refactor: reorganize raise into own folder - src/error/raise/raise.ts Co-Authored-By: Claude Opus 4.7 --- packages/errors/src/error/{ => raise}/raise.ts | 2 +- packages/errors/src/index.ts | 2 +- packages/errors/tests/raise.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename packages/errors/src/error/{ => raise}/raise.ts (94%) diff --git a/packages/errors/src/error/raise.ts b/packages/errors/src/error/raise/raise.ts similarity index 94% rename from packages/errors/src/error/raise.ts rename to packages/errors/src/error/raise/raise.ts index a4aaa6d..efcd3e9 100644 --- a/packages/errors/src/error/raise.ts +++ b/packages/errors/src/error/raise/raise.ts @@ -2,7 +2,7 @@ * Error raising utilities. */ -import type { ErrorInstance } from './types.js'; +import type { ErrorInstance } from '../types.js'; /** * Throws an ErrorInstance. diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index 9215a81..9768ccb 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -16,4 +16,4 @@ export type { export { error } from './error/error.js'; // Error raising function -export { raise } from './error/raise.js'; +export { raise } from './error/raise/raise.js'; diff --git a/packages/errors/tests/raise.test.ts b/packages/errors/tests/raise.test.ts index 9caa4a7..7fc6eda 100644 --- a/packages/errors/tests/raise.test.ts +++ b/packages/errors/tests/raise.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest'; import { error } from '../src/error/error.js'; -import { raise } from '../src/error/raise.js'; +import { raise } from '../src/error/raise/raise.js'; import type { ErrorInstance } from '../src/error/types.js'; describe( 'raise() function', () => { From 371972d3214266ce2c619c2436774c31e43eeed5 Mon Sep 17 00:00:00 2001 From: martyy-code Date: Fri, 29 May 2026 10:06:45 +0200 Subject: [PATCH 3/3] refactor: move raise to src/raise/index.ts Co-Authored-By: Claude Opus 4.7 --- packages/errors/src/index.ts | 2 +- packages/errors/src/{error/raise/raise.ts => raise/index.ts} | 2 +- packages/errors/tests/raise.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename packages/errors/src/{error/raise/raise.ts => raise/index.ts} (94%) diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index 9768ccb..f272999 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -16,4 +16,4 @@ export type { export { error } from './error/error.js'; // Error raising function -export { raise } from './error/raise/raise.js'; +export { raise } from './raise/index.js'; diff --git a/packages/errors/src/error/raise/raise.ts b/packages/errors/src/raise/index.ts similarity index 94% rename from packages/errors/src/error/raise/raise.ts rename to packages/errors/src/raise/index.ts index efcd3e9..456bba9 100644 --- a/packages/errors/src/error/raise/raise.ts +++ b/packages/errors/src/raise/index.ts @@ -2,7 +2,7 @@ * Error raising utilities. */ -import type { ErrorInstance } from '../types.js'; +import type { ErrorInstance } from '../error/types.js'; /** * Throws an ErrorInstance. diff --git a/packages/errors/tests/raise.test.ts b/packages/errors/tests/raise.test.ts index 7fc6eda..4b1db86 100644 --- a/packages/errors/tests/raise.test.ts +++ b/packages/errors/tests/raise.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest'; import { error } from '../src/error/error.js'; -import { raise } from '../src/error/raise/raise.js'; +import { raise } from '../src/raise/index.js'; import type { ErrorInstance } from '../src/error/types.js'; describe( 'raise() function', () => {