Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/agent-memory/typescript-expert/MEMORY.md
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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<TSchema>
: Record<string, never>
>(config: {
name: string;
fields?: TSchema;
inherits?: ErrorFactory | ErrorFactory[];
message?: string;
}): ErrorFactory<TData> => { ... }

// 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<TData>): ErrorInstance<TData> => {
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<string, unknown>,
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 |
3 changes: 3 additions & 0 deletions packages/errors/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,3 +14,6 @@ export type {

// Error factory function
export { error } from './error/error.js';

// Error raising function
export { raise } from './raise/index.js';
44 changes: 44 additions & 0 deletions packages/errors/src/raise/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
/**
* Error raising utilities.
*/

import type { ErrorInstance } from '../error/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 };
107 changes: 107 additions & 0 deletions packages/errors/tests/raise.test.ts
Original file line numberDiff line numberDiff line change
@@ -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/raise/index.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 );
} );
} );
} );