Skip to content

ErrorDetails claims to be a discriminated union but accepts any object #772

Description

@InfinityBowman

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions