Skip to content

TS: make Bundle<T> generic by propagating nested type params#148

Open
ryukzak wants to merge 5 commits intomainfrom
feat/bundle-generic
Open

TS: make Bundle<T> generic by propagating nested type params#148
ryukzak wants to merge 5 commits intomainfrom
feat/bundle-generic

Conversation

@ryukzak
Copy link
Copy Markdown
Collaborator

@ryukzak ryukzak commented Apr 22, 2026

Closes #145.

Summary

  • Parent interfaces now become generic when any of their fields reference a generic nested type. The type parameter is carried through at the reference site.
  • Applies to Bundle (via BundleEntry<T>) and will also kick in for any other resource whose nested types happen to be generic.
  • Default type parameter keeps existing call sites working — zero breakage.

Generated output (before / after)

Motivation: callers couldn't narrow Bundle.entry[i].resource without hand-rolled intersection types.

Before:

export interface Bundle extends Resource {
    resourceType: "Bundle";
    entry?: BundleEntry[];
    // ...
}

After:

export interface Bundle<T extends Resource = Resource> extends Resource {
    resourceType: "Bundle";
    entry?: BundleEntry<T>[];
    // ...
}

Usage

const bundle: Bundle<Patient | Observation> = {
    resourceType: "Bundle",
    type: "transaction",
    entry: [
        { fullUrl: `urn:uuid:${patient.id}`, resource: patient },
        { fullUrl: `urn:uuid:${obs.id}`, resource: obs },
    ],
};

// TS 5.5+ infers the type predicate from the discriminated union — no explicit `r is Observation` needed
const observations = (bundle.entry ?? [])
    .map(e => e.resource)
    .filter(r => r?.resourceType === "Observation"); // Observation[]

Implementation

  • Extracted the generic-params computation in generateType() into a reusable helper computeGenericInfo that works for both nested and top-level schemas.
  • generateNestedTypes now returns a per-nested-type Record<string, GenericInfo> which is threaded back into the parent's generateType call.
  • When a parent field's type points to a generic nested type, the parent adopts the nested's generic params (same names/constraints) and appends the args at the reference site (e.g. BundleEntry<T>[]).
  • Scope is limited to the natural pass-through case — no new family-type detection, no renaming heuristics.

Tests

  • Added unit assertion generates Bundle with generic entry type (nested generic propagation) in typescript.test.ts — directly covers the nested propagation path.
  • Updated resource.test.ts demo to drop the explicit (r): r is Observation predicate; TS 5.5+ infers it automatically from the discriminated union, which is the whole point of the feature.

ryukzak and others added 3 commits April 28, 2026 16:20
Parent interfaces now become generic when any of their fields reference
a generic nested type, and the field reference carries the type
parameter through.

Before: `Bundle { entry?: BundleEntry[] }` (always BundleEntry<Resource>)
After:  `Bundle<T extends Resource = Resource> { entry?: BundleEntry<T>[] }`

Default `= Resource` keeps existing call sites working. Callers can now
narrow, e.g. `Bundle<Patient | Observation>`.

Refactored the generic-params computation in generateType() into a
reusable helper (computeGenericInfo) and threaded per-nested-type info
from generateNestedTypes() back into the parent generation.
- Bundle.ts now declares `Bundle<T extends Resource = Resource>` with
  `entry?: BundleEntry<T>[]`.
- Added demo tests showing discriminated-union narrowing via
  `Bundle<Patient | Observation>` and backwards-compat default.
@ryukzak ryukzak force-pushed the feat/bundle-generic branch from 3868aac to a9cee6f Compare April 28, 2026 14:21
ryukzak added 2 commits April 28, 2026 16:56
Drop the standalone computeGenericInfo helper and the GenericInfo type
in favor of inlining the (small) logic directly into generateType. The
helper duplicated the existing typeFamilyFields detection that already
worked; the genuinely new bit is just the nested-type pass-through.

generateType now returns the parent's GenericParam[] (only paramList is
needed by callers — fieldMap and nestedArgsByField are local concerns),
and generateNestedTypes threads a Record<string, GenericParam[]> back
to the parent.

Net change vs main: +61/-32 (was +81/-36). No behavior change.
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.

Feature: make Bundle<T> generic (BundleEntry<T> already is)

1 participant