From d08011a4a8e94d48fb417a7145d2e8ca7348e094 Mon Sep 17 00:00:00 2001 From: martyy-code Date: Tue, 11 Aug 2026 14:36:05 +0200 Subject: [PATCH] fix(errors): replace chained cast on FACTORY_SYMBOL with typed property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #71: `error.ts` assigned the factory marker via a chained type assertion `(instance as unknown as Record unknown>)[FACTORY_SYMBOL]` — the canonical example of the pattern rule 0008 forbids. Move `FACTORY_SYMBOL` from `error.ts` to `types.ts` (the contract owns the marker, not the implementation) and declare it as a typed property on `ErrorInstance`. The assignment in `error.ts` becomes a single property write on the declared type; the `as unknown` cast is gone. `is/index.ts` updates its import to match the new source. Add a regression test in `error.test.ts` that confirms the marker is attached to a fresh instance and points back to the factory that produced it. Public API unchanged; all 83 tests pass; type-check and lint clean. Closes #71. --- ...x-71-chained-cast-error-instance-marker.md | 5 ++++ packages/errors/src/error/error.ts | 27 ++++--------------- packages/errors/src/error/types.ts | 25 +++++++++++++++++ packages/errors/src/is/index.ts | 2 +- packages/errors/tests/error.test.ts | 14 ++++++++++ 5 files changed, 50 insertions(+), 23 deletions(-) create mode 100644 .changeset/fix-71-chained-cast-error-instance-marker.md 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()', () => {