Skip to content

TypeSchema/TS: safe parse helpers for ValueSet-bound coded fields#154

Open
ryukzak wants to merge 6 commits intomainfrom
feat/coded-field-helpers
Open

TypeSchema/TS: safe parse helpers for ValueSet-bound coded fields#154
ryukzak wants to merge 6 commits intomainfrom
feat/coded-field-helpers

Conversation

@ryukzak
Copy link
Copy Markdown
Collaborator

@ryukzak ryukzak commented May 1, 2026

Closes #142.

TypeSchema

  • BindingTypeSchema carries an optional concepts: Concept[] field with the full {code, system, display} triples extracted from each ValueSet, alongside the existing enum.values (codes only).

TS generator

  • New bindings.ts per FHIR package, listing every binding the package's resources / complex types / profiles reference. For each binding it emits:
    • <Binding>Codes — readonly tuple of valid codes
    • <Binding> — literal-union type alias (typeof <Binding>Codes)[number]
    • <Binding>ConceptsReadonly<Record<<Binding>, { system?, code, display? }>>
    • parse<Binding>(input, fieldName?) — narrows a string to the literal type, throws on unknown values
    • parse<Binding>Coding(input, fieldName?) — fills system/display from the lookup table
  • bindings.ts is re-exported from each package's index.ts.
  • profile-helpers.ts is now copied whenever bindings (not just profiles) are emitted, since the parsers depend on parseLiteral / parseCoding there.

Runtime helpers (profile-helpers.ts)

  • parseLiteral, parseCoding, parseBoolean, parseNumber, parseInstant. Each throws an Error with the field name and allowed values when the input is invalid.

Examples

// before — `as` cast, no runtime check
gender: row.gender as Patient["gender"],

// after — validated at runtime, throws on garbage
gender: parseAdministrativeGender(row.gender, "Patient.gender"),
// before — caller carries system+display
patient.setRace({ ombCategory: { system: "urn:oid:...", code: row.code, display: row.display } });

// after — code only, system/display filled from binding
patient.setRace({ ombCategory: parseOmbRaceCategoryCoding(row.code) });
  • examples/typescript-r4/bindings.test.ts — end-to-end demo using the generated parsers.
  • R4 + US Core fhir-types/ regenerated to include the new bindings.ts module.

ryukzak added 6 commits May 1, 2026 15:31
Add an optional `concepts` field to `BindingTypeSchema` that preserves the
{code, system, display} triples for each enum value. Previously only the bare
code list was stored; system and display were extracted from the ValueSet then
discarded. Generators that want to emit code-to-Coding lookup tables can now
read the concepts directly from the binding instead of re-resolving the
ValueSet.
`bun test --update-snapshots` also reordered the snapshot blocks in
introspection.test.ts.snap to match the test source order; that reorder accounts
for most of the diff (no content delta).
Add `parseLiteral`, `parseCoding`, `parseBoolean`, `parseNumber`, and
`parseInstant` to the runtime helpers asset.  These are the building blocks
for safely converting external string data (CSV, HTTP form, untyped JSON)
into typed FHIR values; per-binding wrappers in the next commit call them
with their lookup tables.
For each generated FHIR package, walk the package's resources, complex
types, and profiles to find all referenced ValueSet bindings, then emit a
`bindings.ts` module containing for each binding:

- a `<Binding>Codes` readonly tuple of valid codes (the literal union)
- a `<Binding>` type alias narrowing to that union
- a `<Binding>Concepts` lookup table (code → {system, code, display})
- `parse<Binding>(input)` returning the literal type
- `parse<Binding>Coding(input)` returning a Coding-shaped object filled in
  from the lookup table

Both parsers throw with the field name and allowed values when the input
is not in the binding.  This eliminates `as` casts on coded fields and
removes the need for callers to re-supply `system` and `display` for
ValueSet-bound Codings.

The per-package bindings module is re-exported from each FHIR package's
index, and `profile-helpers.ts` is now copied whenever bindings (not just
profiles) are emitted, since the parsers depend on it.
- `test/api/write-generator/typescript.test.ts`: assert that the R4
  generation emits `bindings.ts` with the expected `AdministrativeGender`
  codes / concepts / parse helpers, that the package index re-exports the
  bindings module, and that `profile-helpers.ts` is copied alongside.
- `examples/typescript-r4/bindings.test.ts`: end-to-end demo using the
  generated `parseAdministrativeGender` and `parseAdministrativeGenderCoding`
  to convert an untyped CSV-like row into a typed Patient.
- Add `bindings.ts` and `export * from "./bindings"` in each FHIR
  package's index.
- Update copied `profile-helpers.ts` with the new `parse*` runtime
  helpers.
- Refresh US Core profile classes (catches up the recently-added static
  `is()` predicate that the previous regen on this branch missed).
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: safe helpers for coded fields — Literal unions and ValueSet-bound Codings

1 participant