Skip to content

feat(orm): implement discriminated union return types for delegate (polymorphic) models - #2

Merged
motopods merged 2 commits into
devfrom
copilot/update-type-inference-for-content
Apr 2, 2026
Merged

motopods merged 2 commits into
devfrom
copilot/update-type-inference-for-content

Conversation

Copilot AI commented Apr 2, 2026

Copy link
Copy Markdown

Summary

Querying a delegate (polymorphic base) model such as db.asset.findMany() now returns a proper TypeScript discriminated union instead of the flat base type.

Before: findFirstOrThrow() returned Asset — accessing r.duration was always a type error, and narrowing by r.assetType had no effect on the type.

After: findFirstOrThrow() returns:

| (ImageFields   & { assetType: 'Image' })
| (RatedVideoFields & { assetType: 'Video'; videoType: 'RatedVideo' })

TypeScript can now narrow the result via the discriminator field:

const r = await db.asset.findFirstOrThrow();
if (r.assetType === 'Video') {
    console.log(r.duration); // ✅ Video-specific field
    console.log(r.rating);   // ✅ RatedVideo-specific field (only submodel)
} else {
    console.log(r.format);   // ✅ Image-specific field
}

Multi-level delegation (Asset → Video → RatedVideo) is fully expanded so every concrete leaf model appears in the union. The select path continues to return a flat result as before (explicitly selected fields remove polymorphic information).

Changes

packages/orm/src/client/crud-types.ts

  • Added IsDelegateModel, GetModelDiscriminator, GetSubModels to schema imports.
  • Added FlatModelResult helper type — the original scalar-field object expression extracted into a named alias for the leaf case.
  • Added DelegateUnionResult<Schema, Model, Options, SubModel, Omit, _Depth>:
    • Distributes TypeScript's union over each direct sub-model.
    • When a sub-model is itself a delegate, recurses into its sub-models so all concrete leaf types appear with all ancestor discriminator fields fixed to literal values.
    • Uses a tuple-based depth counter (hard stop at 10) as an additional safety net.
  • Modified DefaultModelResult to use DelegateUnionResult for delegate models.
    • Added _IsGenericModel default type parameter (checks [string] extends [Model]) that short-circuits the entire delegate expansion when TypeScript is evaluating ClientContract<SchemaDef> (the generic base interface). This keeps the type instantiation count bounded (~1.9M vs 1.6M baseline, well below TypeScript's recursion limit).

tests/e2e/orm/schemas/delegate/typecheck.ts

  • Uncommented the two discriminated-union narrowing test blocks (for findFirstOrThrow and the include: { assets: true } relation path).

Copilot AI and others added 2 commits April 2, 2026 05:00
@motopods
motopods marked this pull request as ready for review April 2, 2026 08:52
@motopods
motopods merged commit 5d18867 into dev Apr 2, 2026
2 checks passed
@motopods
motopods deleted the copilot/update-type-inference-for-content branch April 4, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants