Problem
Generated FHIR types narrow coded fields at compile time (Literal unions, CodeableConcept<T>), but they stop short of validating or enriching values at runtime. When data flows in from an external source (CSV, HTTP form, another system's JSON), users end up either:
as-casting — compile-time only, no runtime safety.
- Re-supplying information the binding already knows — e.g., carrying
system and display through the pipeline even though they're derivable from the ValueSet binding.
Two concrete pain points surfaced in the tutorial US Core Profiles in TypeScript with @atomic-ehr/codegen. Example code: HealthSamurai/examples — atomic-ehr-codegen-typescript-us-core-profiles.
Pain point 1: as cast on narrow Literal unions
const basePatient: Patient = {
resourceType: "Patient",
identifier: [...],
name: [...],
gender: row.gender as Patient["gender"], // ❌ "FEMALE" / "Female" / "mal" / anything — slips through
birthDate: row.birthDate,
};
Patient["gender"] is "male" | "female" | "other" | "unknown" | undefined. The as cast says "trust me" with no runtime check. A typo in the CSV silently produces an invalid Patient that fails at POST time or, worse, serializes with garbage.
Pain point 2: Manual system / display on ValueSet-bound Codings
patient.setRace({
ombCategory: {
system: "urn:oid:2.16.840.1.113883.6.238",
code: row.raceCode,
display: row.raceDisplay,
},
text: row.raceDisplay,
});
The ombCategory field in the US Core Race extension is bound to the OMB Race Categories ValueSet. The generator already knows:
2106-3 → system urn:oid:2.16.840.1.113883.6.238, display "White"
2054-5 → system urn:oid:2.16.840.1.113883.6.238, display "Black or African American"
- ...
Yet the caller has to carry system, display, and code through every hop (CSV columns, DB rows, intermediate JSON). That's:
- Duplicated information — the binding knows it; the call site shouldn't re-state it.
- A drift vector — subtle display mismatches ("White" vs "white" vs "White or European") slip in and nobody notices until someone searches or audits.
- No validation —
row.raceCode = "XYZ-99" passes tsc happily and posts to the server, where it may or may not be caught.
Proposal
Emit helpers that know the ValueSet binding at codegen time.
For Literal unions (pain point 1)
Per-binding parse* helpers that throw on unknown values:
// Generated alongside Patient.ts
export const parsePatientGender = (input: string): Patient["gender"] => {
const valid = ["male", "female", "other", "unknown"] as const;
if ((valid as readonly string[]).includes(input)) return input as Patient["gender"];
throw new Error(`Invalid Patient.gender: ${JSON.stringify(input)}. Expected one of: ${valid.join(", ")}`);
};
Or a generic helper in profile-helpers.ts:
import { parseLiteral } from "./fhir-types/profile-helpers";
gender: parseLiteral(row.gender, ["male", "female", "other", "unknown"] as const, "Patient.gender")
For ValueSet-bound Codings (pain point 2)
Code-to-Coding helpers that fill system and display from the bound ValueSet:
// Generated alongside Extension_USCoreRaceExtension.ts
export const USCoreOmbRaceCategories = {
"2106-3": { system: "urn:oid:2.16.840.1.113883.6.238", code: "2106-3", display: "White" },
"2054-5": { system: "urn:oid:2.16.840.1.113883.6.238", code: "2054-5", display: "Black or African American" },
// ...
} as const;
export const parseOmbRaceCategory = (input: string): Coding => {
if (input in USCoreOmbRaceCategories) {
return USCoreOmbRaceCategories[input as keyof typeof USCoreOmbRaceCategories];
}
throw new Error(`Invalid OMB race category code: ${JSON.stringify(input)}`);
};
Usage (the row.raceSystem / row.raceDisplay CSV columns become unnecessary):
patient.setRace({ ombCategory: parseOmbRaceCategory(row.raceCode) });
Or, if setters accept a code-only shorthand directly, even simpler:
patient.setRace({ ombCategoryCode: row.raceCode }); // system/display looked up at call time
(Trade-off for the shorthand form: loses the ability to override display if the caller wants a local-language label.)
Bonus: primitive parsers
Same pattern applies to numeric primitives and booleans — a tiny set of parseNumber / parseBoolean / parseInstant helpers in profile-helpers.ts would cover most CSV ingestion cases.
Scope and trade-offs
- Helpers make sense for bindings with
strength = required | extensible. preferred / example bindings could get the helper with a warning, or skip.
- Large ValueSets (thousands of codes, SNOMED, LOINC) probably shouldn't emit the full lookup table — either validation-only (
isInValueSet(code)), or skip and fall back to plain Coding input.
CodeableConcept<T> already narrows the type parameter for small bound ValueSets; this proposal adds the runtime validation + enrichment layer on top.
Context
Both pain points surfaced in the tutorial US Core Profiles in TypeScript with @atomic-ehr/codegen. CSV-to-Bundle ingestion is one of the most common FHIR real-world patterns and the current story is "write manual validators or as-cast and pray". A ValueSet-aware helper layer would make the generated types meaningfully safer without forcing callers to restate what the generator already knows.
Related: #141 (Reference type narrowness) — same theme of safe boundaries between external string data and typed FHIR values.
Problem
Generated FHIR types narrow coded fields at compile time (
Literalunions,CodeableConcept<T>), but they stop short of validating or enriching values at runtime. When data flows in from an external source (CSV, HTTP form, another system's JSON), users end up either:as-casting — compile-time only, no runtime safety.systemanddisplaythrough the pipeline even though they're derivable from the ValueSet binding.Two concrete pain points surfaced in the tutorial US Core Profiles in TypeScript with @atomic-ehr/codegen. Example code: HealthSamurai/examples — atomic-ehr-codegen-typescript-us-core-profiles.
Pain point 1:
ascast on narrow Literal unionsPatient["gender"]is"male" | "female" | "other" | "unknown" | undefined. Theascast says "trust me" with no runtime check. A typo in the CSV silently produces an invalid Patient that fails at POST time or, worse, serializes with garbage.Pain point 2: Manual
system/displayon ValueSet-bound CodingsThe
ombCategoryfield in the US Core Race extension is bound to the OMB Race Categories ValueSet. The generator already knows:2106-3→ systemurn:oid:2.16.840.1.113883.6.238, display"White"2054-5→ systemurn:oid:2.16.840.1.113883.6.238, display"Black or African American"Yet the caller has to carry
system,display, andcodethrough every hop (CSV columns, DB rows, intermediate JSON). That's:row.raceCode = "XYZ-99"passestschappily and posts to the server, where it may or may not be caught.Proposal
Emit helpers that know the ValueSet binding at codegen time.
For Literal unions (pain point 1)
Per-binding
parse*helpers that throw on unknown values:Or a generic helper in
profile-helpers.ts:For ValueSet-bound Codings (pain point 2)
Code-to-Coding helpers that fill
systemanddisplayfrom the bound ValueSet:Usage (the
row.raceSystem/row.raceDisplayCSV columns become unnecessary):Or, if setters accept a code-only shorthand directly, even simpler:
(Trade-off for the shorthand form: loses the ability to override
displayif the caller wants a local-language label.)Bonus: primitive parsers
Same pattern applies to numeric primitives and booleans — a tiny set of
parseNumber/parseBoolean/parseInstanthelpers inprofile-helpers.tswould cover most CSV ingestion cases.Scope and trade-offs
strength = required | extensible.preferred/examplebindings could get the helper with a warning, or skip.isInValueSet(code)), or skip and fall back to plainCodinginput.CodeableConcept<T>already narrows the type parameter for small bound ValueSets; this proposal adds the runtime validation + enrichment layer on top.Context
Both pain points surfaced in the tutorial US Core Profiles in TypeScript with @atomic-ehr/codegen. CSV-to-Bundle ingestion is one of the most common FHIR real-world patterns and the current story is "write manual validators or
as-cast and pray". A ValueSet-aware helper layer would make the generated types meaningfully safer without forcing callers to restate what the generator already knows.Related: #141 (Reference type narrowness) — same theme of safe boundaries between external string data and typed FHIR values.