ErrorDetails is documented as a discriminated union but is structurally incapable of discriminating, and accepts any object at all.
The mismatch
packages/shared/src/errors/types.ts:27-30:
// Error details - strongly typed based on error code
// Each error code has a specific details shape
// Runtime: details shape validated based on error code
// TypeScript: discriminated union based on code for type safety
export type ErrorDetails =
| ValidationErrorDetails | ProjectErrorDetails | FileErrorDetails
| AuthErrorDetails | SystemErrorDetails;
None of that holds:
- Every member is all-optional, so
{} satisfies all five.
- Four of the five (
Project, File, Auth, System) also carry [key: string]: unknown, so the union accepts any object.
- There is no discriminant inside the union.
code lives on DomainError, outside it. Narrowing is impossible by construction.
- Separately,
ValidationErrorCode through SystemErrorCode are each export type X = string, so DomainErrorCode resolves to string and the // Typed error code comment on DomainError.code:15 describes nothing.
Practical effect
createDomainError(AUTH_ERRORS.FORBIDDEN, { reasn: 'quota_exceeded' }) // typo compiles clean
And no consumer can narrow details to discover what is in it.
The error codes are safe -- createDomainError takes an ErrorDefinition object rather than a bare string, so a typo there does not compile. It is the details bag that is untyped in practice.
Scope, reduced 2026-09-12
There are 100 createDomainError( producers and zero consumers that narrow details (no .details.reason, .details.field, etc. anywhere in packages/web/src). A discriminated union serves readers, and there are none yet. So the cheap two-thirds of the fix ships now and the correlated union waits for a consumer.
Done when
DomainErrorCode is a real union derived from the error definition objects, not string, so DomainError.code is actually typed
- Index signatures removed from the four details interfaces, or each one justified in a comment
- The comments describe what the code does
- Deferred, tracked here: members carry a discriminant or
code and details are correlated. Do this when the first consumer needs to narrow details.
Risk
Low-medium. Removing the index signatures surfaces every mistyped or ad-hoc details payload across the 100 producers, which is the point but is also the cost.
Effort: 2-3 hours including fallout. Falls under the Source of Truth Policy in .claude/CLAUDE.md -- the comments currently describe a design the types do not implement.
Part of #778 (TypeScript correctness target state and tracker).
ErrorDetailsis documented as a discriminated union but is structurally incapable of discriminating, and accepts any object at all.The mismatch
packages/shared/src/errors/types.ts:27-30:None of that holds:
{}satisfies all five.Project,File,Auth,System) also carry[key: string]: unknown, so the union accepts any object.codelives onDomainError, outside it. Narrowing is impossible by construction.ValidationErrorCodethroughSystemErrorCodeare eachexport type X = string, soDomainErrorCoderesolves tostringand the// Typed error codecomment onDomainError.code:15describes nothing.Practical effect
And no consumer can narrow
detailsto discover what is in it.The error codes are safe --
createDomainErrortakes anErrorDefinitionobject rather than a bare string, so a typo there does not compile. It is thedetailsbag that is untyped in practice.Scope, reduced 2026-09-12
There are 100
createDomainError(producers and zero consumers that narrowdetails(no.details.reason,.details.field, etc. anywhere inpackages/web/src). A discriminated union serves readers, and there are none yet. So the cheap two-thirds of the fix ships now and the correlated union waits for a consumer.Done when
DomainErrorCodeis a real union derived from the error definition objects, notstring, soDomainError.codeis actually typedcodeanddetailsare correlated. Do this when the first consumer needs to narrowdetails.Risk
Low-medium. Removing the index signatures surfaces every mistyped or ad-hoc details payload across the 100 producers, which is the point but is also the cost.
Effort: 2-3 hours including fallout. Falls under the Source of Truth Policy in
.claude/CLAUDE.md-- the comments currently describe a design the types do not implement.Part of #778 (TypeScript correctness target state and tracker).