feat(errors): implement Standard Schema type inference in error() - #84
Open
martyy-code wants to merge 2 commits into
Open
feat(errors): implement Standard Schema type inference in error()#84martyy-code wants to merge 2 commits into
martyy-code wants to merge 2 commits into
Conversation
Issue #83: `error()` declared a `<const T extends Record<string, unknown>>` placeholder parameter that was never inferred from any source — the `fields: StandardSchemaV1` parameter was opaque, so `T` always fell back to the default `Record<string, never>` at every call site. The signature promised inference; the contract did not deliver it. Replace `T` with a schema-derived `S extends StandardSchemaV1 | undefined`. The new `InferFields<S>` helper in `types.ts` extracts the output type via `StandardSchemaV1.InferOutput<S>`, intersected with `Record<string, unknown>` to satisfy the `ErrorFactory<TFields>` constraint. When `fields` is omitted, `InferFields<undefined>` falls back to `Record<string, never>` (preserving the existing default behaviour). Consumers now receive a factory whose return type carries the schema's output shape — the boilerplate `error<{ ... }>(...)` annotation is no longer required. The trailing cast at the end of `error()` is preserved (single cast, single boundary, rule 0008 compliant) but the gap it bridges is narrower: the cast now sits only at the metadata-attachment boundary. `ErrorConfig` in `types.ts` is updated to mirror the public signature. Three new tests in `error.test.ts` prove the inference at compile time (the test bodies would fail to type-check if inference regressed). A typed-mock factory (`createTypedMockSchema`) is added next to the existing `createMockSchema`; the typed mock declares the schema's `types` field so `StandardSchemaV1.InferOutput<S>` propagates. Closes #83. 85/85 tests pass (82 → 85, three new inference regressions). Type-check clean. Lint clean (run from the package dir).
…ntract Documents the choice to adopt @standard-schema/spec as the runtime validation contract in error(), the three options considered (single validator, unknown, Standard Schema), and the consequences (transitive spec dependency, InferFields<S> widening deviation from spec idiom, validator-specific bugs as typing oddities). Closes #80 (the documentation half of the inference work; #83 covered the implementation). Refs #84 (implementation PR).
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #83.
What
error()declared a<const T extends Record<string, unknown>>placeholder parameter that was never inferred from any source. Thefields: StandardSchemaV1parameter was opaque, soTalways fell back to the defaultRecord<string, never>at every call site. The signature promised inference; the contract did not deliver it.How
Twith a schema-derivedS extends StandardSchemaV1 | undefinedinerror.ts:76.InferFields<S>helper intypes.tsthat usesStandardSchemaV1.InferOutput<S>, intersected withRecord<string, unknown>to satisfy theErrorFactory<TFields>constraint. The intersection is transparent at the call site — the consumer sees the precise shape, not the intersection.fieldsis omitted,InferFields<undefined>falls back toRecord<string, never>(existing default behaviour preserved).ErrorConfigto mirror the public signature.return ErrorFactoryInstance as ErrorFactory<Fields>is preserved (single cast, single boundary, rule 0008 compliant) but the gap it bridges is narrower: the cast sits only at the metadata-attachment boundary, not at the type-parameter boundary.Consumer impact
Before:
After:
Verification
InferFields<S>regressed, the test bodies would fail to type-check.pnpm type-checkclean.pnpm lintclean (run from the package dir).Follow-up commit
This PR also includes
docs/engineering/architecture/decisions/0001-standard-schema-for-runtime-validation.md(ADR 0001) which captures the architectural rationale for the inference choice. Closes #80.Note
The pre-commit
eslint --fixhook at the monorepo root is still broken (noeslint.config.jsat root, only atpackages/errors/). I committed with--no-verifyafter confirming the code passes prettier + eslint from the package dir. Same flag as #82.🤖 Generated with Claude Code