diff --git a/.changeset/fix-71-chained-cast-error-instance-marker.md b/.changeset/fix-71-chained-cast-error-instance-marker.md new file mode 100644 index 0000000..fe71e36 --- /dev/null +++ b/.changeset/fix-71-chained-cast-error-instance-marker.md @@ -0,0 +1,5 @@ +--- +"@deessejs/errors": patch +--- + +Replace the chained type assertion `(instance as unknown as Record unknown>)[FACTORY_SYMBOL]` in `error.ts` with a single property assignment on the declared `ErrorInstance` type. The `FACTORY_SYMBOL` is now declared on `ErrorInstance` 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. diff --git a/packages/errors/src/error/error.ts b/packages/errors/src/error/error.ts index 9f420a8..61d1a6f 100644 --- a/packages/errors/src/error/error.ts +++ b/packages/errors/src/error/error.ts @@ -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 // ============================================================================ @@ -125,10 +114,10 @@ export const error = = Record unknown>)[FACTORY_SYMBOL] = - ErrorFactoryInstance; + // Mark this instance as created by this factory (for is() checks). + // The marker is declared on ErrorInstance in types.ts, so the + // assignment is type-checked without an `as unknown` cast. + instance[FACTORY_SYMBOL] = ErrorFactoryInstance; return instance; }; @@ -155,9 +144,3 @@ export const error = = Record; }; - -// ============================================================================ -// Exports for is() function -// ============================================================================ - -export { FACTORY_SYMBOL }; diff --git a/packages/errors/src/error/types.ts b/packages/errors/src/error/types.ts index 41ab0f6..f63853a 100644 --- a/packages/errors/src/error/types.ts +++ b/packages/errors/src/error/types.ts @@ -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 // ============================================================================ @@ -81,6 +98,14 @@ export type ErrorInstance = Record | 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; }; /** diff --git a/packages/errors/src/is/index.ts b/packages/errors/src/is/index.ts index 5c1aef8..2141cf7 100644 --- a/packages/errors/src/is/index.ts +++ b/packages/errors/src/is/index.ts @@ -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. diff --git a/packages/errors/tests/error.test.ts b/packages/errors/tests/error.test.ts index c885ea5..5ce3f36 100644 --- a/packages/errors/tests/error.test.ts +++ b/packages/errors/tests/error.test.ts @@ -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 (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)[FACTORY_SYMBOL]; + + expect(marker).toBe(TestError); + }); }); describe('.addNote()', () => {