Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/fix-71-chained-cast-error-instance-marker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@deessejs/errors": patch
---

Replace the chained type assertion `(instance as unknown as Record<typeof FACTORY_SYMBOL, () => unknown>)[FACTORY_SYMBOL]` in `error.ts` with a single property assignment on the declared `ErrorInstance<T>` type. The `FACTORY_SYMBOL` is now declared on `ErrorInstance<T>` in `types.ts`; the marker assignment is type-checked, and the `as unknown` cast is gone. Closes #71. The package's public API is unchanged; all 83 tests pass.
27 changes: 5 additions & 22 deletions packages/errors/src/error/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,10 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';

import type { ErrorFactory, ErrorInstance } from './types.js';
import { FACTORY_SYMBOL } from './types.js';
import { captureStack } from './capture.js';
import { formatTemplate, hasTemplatePlaceholders } from './format.js';

// ============================================================================
// Symbols for identity
// ============================================================================

/**
* Symbol used to identify factory-created errors.
* Stored on the error instance to enable reliable instanceof checks.
*
* @internal
*/
const FACTORY_SYMBOL = Symbol.for('@deessejs/errors/factory');

// ============================================================================
// Error Factory
// ============================================================================
Expand Down Expand Up @@ -125,10 +114,10 @@ export const error = <const T extends Record<string, unknown> = Record<string, n
return instance;
};

// Mark this instance as created by this factory (for is() checks)
// Use callable to avoid generic parameter conflicts
(instance as unknown as Record<typeof FACTORY_SYMBOL, () => unknown>)[FACTORY_SYMBOL] =
ErrorFactoryInstance;
// Mark this instance as created by this factory (for is() checks).
// The marker is declared on ErrorInstance<T> in types.ts, so the
// assignment is type-checked without an `as unknown` cast.
instance[FACTORY_SYMBOL] = ErrorFactoryInstance;

return instance;
};
Expand All @@ -155,9 +144,3 @@ export const error = <const T extends Record<string, unknown> = Record<string, n

return ErrorFactoryInstance as ErrorFactory<T>;
};

// ============================================================================
// Exports for is() function
// ============================================================================

export { FACTORY_SYMBOL };
25 changes: 25 additions & 0 deletions packages/errors/src/error/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@

import type { StandardSchemaV1 } from '@standard-schema/spec';

// ============================================================================
// Internal markers
// ============================================================================

/**
* Symbol used to identify factory-created errors.
* Stored on the error instance to enable reliable instanceof checks.
*
* The symbol is shared across the package (registered in the global
* Symbol registry) so that two copies of the package — or two
* realms — agree on the marker. The runtime check in `is()` is
* keyed by this symbol.
*
* @internal
*/
export const FACTORY_SYMBOL: unique symbol = Symbol.for('@deessejs/errors/factory');

// ============================================================================
// Types
// ============================================================================
Expand Down Expand Up @@ -81,6 +98,14 @@ export type ErrorInstance<TFields extends Record<string, unknown> = Record<strin
context: Record<string, unknown> | null;
/** Parent error factories for type checking */
inherits?: ErrorFactory | ErrorFactory[];
/**
* Marker pointing back to the factory that produced this instance.
* Set by `error()` at construction time; read by `is()` to
* discriminate factory-created errors.
*
* @internal
*/
[FACTORY_SYMBOL]: ErrorFactory<TFields>;
};

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/errors/src/is/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import type { ErrorFactory, ErrorInstance } from '../error/types.js';
import { FACTORY_SYMBOL } from '../error/error.js';
import { FACTORY_SYMBOL } from '../error/types.js';

/**
* Type to extract the fields from an ErrorFactory or native Error class.
Expand Down
14 changes: 14 additions & 0 deletions packages/errors/tests/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,20 @@ describe('error() factory function', () => {

expect(instance.inherits).toBe(ParentError);
});

it('should attach the factory marker without a chained cast', () => {
const TestError = error({ name: 'TestError' });
const instance = TestError();

// The marker is a typed property on ErrorInstance<T> (no
// `as unknown` cast at the call site). It points back to the
// factory that produced the instance, and `is()` reads it.
// Regression for issue #71.
const FACTORY_SYMBOL = Symbol.for('@deessejs/errors/factory');
const marker = (instance as unknown as Record<symbol, unknown>)[FACTORY_SYMBOL];

expect(marker).toBe(TestError);
});
});

describe('.addNote()', () => {
Expand Down
Loading