From a0a5650f7d19d1f6521ce367a825e6b6b2487b37 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 16:26:23 +0100 Subject: [PATCH 1/9] Split dependency types: Identifier[] for specializations, TypeIdentifier[] for profiles - SpecializationTypeSchema.dependencies is now Identifier[] (no nested) - ProfileTypeSchema.dependencies remains TypeIdentifier[] (includes nested) - Extract collectRawDeps helper; extractDependencies filters nested, extractProfileDependencies keeps them - Split transformFhirSchema into profile/specialization branches - Split treeShakeTypeSchema dependency recalculation by schema kind - Add nestedIndex fallback in resolveByUrl for constrainedChoice - Remove dead isNestedIdentifier/kind=nested branches in TS writer - Add casts in Python, C#, tree-shake for NestedTypeSchema boundaries --- src/api/writer-generator/python.ts | 7 +- src/api/writer-generator/typescript/writer.ts | 23 ++---- src/typeschema/core/transformer.ts | 82 +++++++++++++------ src/typeschema/ir/tree-shake.ts | 24 +++--- src/typeschema/types.ts | 2 +- src/typeschema/utils.ts | 3 +- .../__snapshots__/typescript.test.ts.snap | 16 ++-- 7 files changed, 95 insertions(+), 62 deletions(-) diff --git a/src/api/writer-generator/python.ts b/src/api/writer-generator/python.ts index 7c87b9795..c39806d79 100644 --- a/src/api/writer-generator/python.ts +++ b/src/api/writer-generator/python.ts @@ -167,8 +167,11 @@ export class Python extends Writer { override async generate(tsIndex: TypeSchemaIndex): Promise { this.tsIndex = tsIndex; const groups: TypeSchemaPackageGroups = { - groupedComplexTypes: groupByPackages(tsIndex.collectComplexTypes()), - groupedResources: groupByPackages(tsIndex.collectResources()), + groupedComplexTypes: groupByPackages(tsIndex.collectComplexTypes()) as Record< + string, + SpecializationTypeSchema[] + >, + groupedResources: groupByPackages(tsIndex.collectResources()) as Record, }; this.generateRootPackages(groups); this.generateSDKPackages(groups); diff --git a/src/api/writer-generator/typescript/writer.ts b/src/api/writer-generator/typescript/writer.ts index 7cc31cb7b..12612317c 100644 --- a/src/api/writer-generator/typescript/writer.ts +++ b/src/api/writer-generator/typescript/writer.ts @@ -7,12 +7,10 @@ import { isChoiceDeclarationField, isComplexTypeIdentifier, isLogicalTypeSchema, - isNestedIdentifier, isPrimitiveIdentifier, isProfileTypeSchema, isResourceTypeSchema, isSpecializationTypeSchema, - type Name, packageMeta, packageMetaToFhir, type SpecializationTypeSchema, @@ -156,14 +154,6 @@ export class TypeScript extends Writer { name: tsResourceName(dep), dep: dep, }); - } else if (isNestedIdentifier(dep)) { - const ndep = { ...dep }; - ndep.name = tsNameFromCanonical(dep.url) as Name; - imports.push({ - tsPackage: `${importPrefix}${tsModulePath(ndep)}`, - name: tsResourceName(dep), - dep: dep, - }); } else { skipped.push(dep); } @@ -214,8 +204,6 @@ export class TypeScript extends Writer { const genericTypes = ["Reference", "Coding", "CodeableConcept"]; if (genericTypes.includes(schema.identifier.name)) { name = `${schema.identifier.name}`; - } else if (schema.identifier.kind === "nested") { - name = tsResourceName(schema.identifier); } else { name = tsResourceName(schema.identifier); } @@ -337,18 +325,19 @@ export class TypeScript extends Writer { }); }); } else if (["complex-type", "resource", "logical"].includes(schema.identifier.kind)) { + const resourceSchema = schema as SpecializationTypeSchema; this.cat(`${tsModuleFileName(schema.identifier)}`, () => { this.generateDisclaimer(); - this.generateDependenciesImports(tsIndex, schema); - this.generateComplexTypeReexports(schema); - this.generateNestedTypes(tsIndex, schema); + this.generateDependenciesImports(tsIndex, resourceSchema); + this.generateComplexTypeReexports(resourceSchema); + this.generateNestedTypes(tsIndex, resourceSchema); this.comment( "CanonicalURL:", schema.identifier.url, `(pkg: ${packageMetaToFhir(packageMeta(schema))})`, ); - this.generateType(tsIndex, schema); - this.generateResourceTypePredicate(schema); + this.generateType(tsIndex, resourceSchema); + this.generateResourceTypePredicate(resourceSchema); }); } else { throw new Error(`Profile generation not implemented for kind: ${schema.identifier.kind}`); diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index e52c121d8..3056bf53c 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -12,9 +12,10 @@ import { concatIdentifiers, extractExtensionDeps, type Field, + type Identifier, isNestedIdentifier, - isProfileIdentifier, type NestedTypeSchema, + type ProfileIdentifier, packageMetaToFhir, type RichFHIRSchema, type RichValueSet, @@ -92,26 +93,45 @@ export async function transformValueSet( }; } -export function extractDependencies( - identifier: TypeIdentifier, +function collectRawDeps( base: TypeIdentifier | undefined, fields: Record | undefined, nestedTypes: NestedTypeSchema[] | undefined, -): TypeIdentifier[] | undefined { - const deps = []; +): TypeIdentifier[] { + const deps: TypeIdentifier[] = []; if (base) deps.push(base); if (fields) deps.push(...extractFieldDependencies(fields)); if (nestedTypes) deps.push(...extractNestedDependencies(nestedTypes)); + return deps; +} - const localNestedTypeUrls = new Set(nestedTypes?.map((nt) => nt.identifier.url)); +export function extractDependencies( + identifier: TypeIdentifier, + base: TypeIdentifier | undefined, + fields: Record | undefined, + nestedTypes: NestedTypeSchema[] | undefined, +): Identifier[] | undefined { + const deps = collectRawDeps(base, fields, nestedTypes); - const filtered = deps.filter((dep) => { + const filtered = deps.filter((dep): dep is Identifier => { if (dep.url === identifier.url) return false; - if (isProfileIdentifier(identifier)) return true; - if (!isNestedIdentifier(dep)) return true; - return !localNestedTypeUrls.has(dep.url); + if (isNestedIdentifier(dep)) return false; + return true; }); + return concatIdentifiers(filtered) as Identifier[] | undefined; +} + +export function extractProfileDependencies( + identifier: TypeIdentifier, + base: TypeIdentifier | undefined, + fields: Record | undefined, + nestedTypes: NestedTypeSchema[] | undefined, +): TypeIdentifier[] | undefined { + const deps = collectRawDeps(base, fields, nestedTypes); + + const filtered = deps.filter((dep) => dep.url !== identifier.url); + return concatIdentifiers(filtered); } @@ -134,21 +154,33 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche const fields = mkFields(register, fhirSchema, [], fhirSchema.elements, logger); const nested = mkNestedTypes(register, fhirSchema, logger); - const extensions = - fhirSchema.derivation === "constraint" ? extractProfileExtensions(register, fhirSchema, logger) : undefined; - const extensionDeps = extensions?.flatMap(extractExtensionDeps); - const dependencies = concatIdentifiers(extractDependencies(identifier, base, fields, nested), extensionDeps); - - const typeSchema: TypeSchema = { - identifier, - base, - fields, - nested, - description: fhirSchema.description, - dependencies, - extensions, - typeFamily: undefined, // NOTE: should be populateTypeFamily later. - }; + let typeSchema: TypeSchema; + if (fhirSchema.derivation === "constraint") { + if (!base) throw new Error(`Profile ${fhirSchema.url} must have a base type`); + const extensions = extractProfileExtensions(register, fhirSchema, logger); + const extensionDeps = extensions?.flatMap(extractExtensionDeps); + const rawDeps = extractProfileDependencies(identifier, base, fields, nested); + typeSchema = { + identifier: identifier as ProfileIdentifier, + base, + fields, + nested, + description: fhirSchema.description, + dependencies: concatIdentifiers(rawDeps, extensionDeps), + extensions, + }; + } else { + const rawDeps = extractDependencies(identifier, base, fields, nested); + typeSchema = { + identifier, + base, + fields, + nested, + description: fhirSchema.description, + dependencies: rawDeps, + typeFamily: undefined, // NOTE: should be populateTypeFamily later. + } as TypeSchema; + } const bindingSchemas = collectBindingSchemas(register, fhirSchema, logger); return [typeSchema, ...bindingSchemas]; diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index 540a4e8ac..a53867e0d 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import type { CodegenLog } from "@root/utils/log"; -import { extractDependencies } from "../core/transformer"; +import { extractDependencies, extractProfileDependencies } from "../core/transformer"; import { type CanonicalUrl, concatIdentifiers, @@ -193,12 +193,12 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo if (rule.selectFields) { if (rule.ignoreFields) throw new Error("Cannot use both ignoreFields and selectFields in the same rule"); - mutableSelectFields(schema, rule.selectFields); + mutableSelectFields(schema as SpecializationTypeSchema, rule.selectFields); } if (rule.ignoreFields) { if (rule.selectFields) throw new Error("Cannot use both ignoreFields and selectFields in the same rule"); - mutableIgnoreFields(schema, rule.ignoreFields); + mutableIgnoreFields(schema as SpecializationTypeSchema, rule.ignoreFields); } if (isProfileTypeSchema(schema) && rule.ignoreExtensions) { @@ -221,15 +221,19 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo } }); }; - collectUsedNestedTypes(schema); + collectUsedNestedTypes(schema as SpecializationTypeSchema); schema.nested = schema.nested.filter((n) => usedTypes.has(n.identifier.url)); } - const extDeps = isProfileTypeSchema(schema) ? schema.extensions?.flatMap(extractExtensionDeps) : undefined; - schema.dependencies = concatIdentifiers( - extractDependencies(schema.identifier, schema.base, schema.fields, schema.nested), - extDeps, - ); + if (isProfileTypeSchema(schema)) { + const extDeps = schema.extensions?.flatMap(extractExtensionDeps); + schema.dependencies = concatIdentifiers( + extractProfileDependencies(schema.identifier, schema.base, schema.fields, schema.nested), + extDeps, + ); + } else { + schema.dependencies = extractDependencies(schema.identifier, schema.base, schema.fields, schema.nested); + } return schema; }; @@ -267,7 +271,7 @@ export const treeShake = (tsIndex: TypeSchemaIndex, treeShake: TreeShakeConf): T for (const nest of schema.nested) { if (isNestedIdentifier(nest.identifier)) continue; const id = JSON.stringify(nest.identifier); - if (!acc[id]) newSchemas.push(nest); + if (!acc[id]) newSchemas.push(nest as unknown as TypeSchema); } } } diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 964509d3e..e27867a4d 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -271,7 +271,7 @@ export interface SpecializationTypeSchema { description?: string; fields?: { [k: string]: Field }; nested?: NestedTypeSchema[]; - dependencies?: TypeIdentifier[]; + dependencies?: Identifier[]; /** Transitive children grouped by kind (e.g. Resource → { resources: [DomainResource, Patient, …] }) */ typeFamily?: { resources?: ResourceIdentifier[]; diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index 6d7803d3c..50c9dd5c8 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -208,7 +208,7 @@ export const mkTypeSchemaIndex = ( const nurl = nschema.identifier.url; const npkg = nschema.identifier.package; nestedIndex[nurl] ??= {}; - nestedIndex[nurl][npkg] = nschema; + nestedIndex[nurl][npkg] = nschema as unknown as TypeSchema; }); } } @@ -231,6 +231,7 @@ export const mkTypeSchemaIndex = ( } } if (index[url]?.[pkgName]) return index[url]?.[pkgName]; + if (nestedIndex[url]?.[pkgName]) return nestedIndex[url]?.[pkgName]; logger?.dryWarn(`Type '${url}' not found in '${pkgName}'`); // Fallback: search across all packages when type exists elsewhere diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index 76015e356..ff9f09770 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -533,8 +533,8 @@ import type { Quantity } from "../../hl7-fhir-r4-core/Quantity"; import type { Reference } from "../../hl7-fhir-r4-core/Reference"; export type Observation_bp_Category_VSCatSliceFlat = Omit; -export type Observation_bp_Component_SystolicBPSliceFlat = Omit; -export type Observation_bp_Component_DiastolicBPSliceFlat = Omit; +export type Observation_bp_Component_SystolicBPSliceFlat = Omit & Quantity; +export type Observation_bp_Component_DiastolicBPSliceFlat = Omit & Quantity; import { buildResource, @@ -545,6 +545,8 @@ import { getArraySlice, ensureSliceDefaults, stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, validateRequired, validateExcluded, validateFixedValue, @@ -714,7 +716,8 @@ export class observation_bpProfile { setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) return this } - const value = applySliceMatch(input ?? {}, match) + const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") + const value = applySliceMatch(wrapped, match) setArraySlice(this.resource.component ??= [], match, value) return this } @@ -725,7 +728,8 @@ export class observation_bpProfile { setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) return this } - const value = applySliceMatch(input ?? {}, match) + const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") + const value = applySliceMatch(wrapped, match) setArraySlice(this.resource.component ??= [], match, value) return this } @@ -749,7 +753,7 @@ export class observation_bpProfile { const item = getArraySlice(this.resource.component, match) if (!item) return undefined if (mode === 'raw') return item - return stripMatchKeys(item, ["code"]) + return unwrapSliceChoice(item, ["code"], "valueQuantity") } public getDiastolicBP(mode: 'flat'): Observation_bp_Component_DiastolicBPSliceFlat | undefined; @@ -760,7 +764,7 @@ export class observation_bpProfile { const item = getArraySlice(this.resource.component, match) if (!item) return undefined if (mode === 'raw') return item - return stripMatchKeys(item, ["code"]) + return unwrapSliceChoice(item, ["code"], "valueQuantity") } // Validation From 52052cfd7e2715632362ab981d5119eb11127fa0 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 16:34:01 +0100 Subject: [PATCH 2/9] ref: Replace schema.identifier.kind checks with type predicates --- src/api/writer-generator/python.ts | 12 +++++++++--- src/api/writer-generator/typescript/writer.ts | 4 ++-- src/typeschema/ir/logic-promotion.ts | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/api/writer-generator/python.ts b/src/api/writer-generator/python.ts index c39806d79..3fa79ae44 100644 --- a/src/api/writer-generator/python.ts +++ b/src/api/writer-generator/python.ts @@ -5,7 +5,13 @@ import { fileURLToPath } from "node:url"; import { camelCase, pascalCase, snakeCase, uppercaseFirstLetterOfEach } from "@root/api/writer-generator/utils"; import { Writer, type WriterOptions } from "@root/api/writer-generator/writer.ts"; import { groupByPackages, sortAsDeclarationSequence, type TypeSchemaIndex } from "@root/typeschema/utils"; -import type { EnumDefinition, Field, SpecializationTypeSchema, TypeIdentifier } from "@typeschema/types.ts"; +import { + type EnumDefinition, + type Field, + isResourceTypeSchema, + type SpecializationTypeSchema, + type TypeIdentifier, +} from "@typeschema/types.ts"; const PRIMITIVE_TYPE_MAP: Record = { boolean: "bool", @@ -433,13 +439,13 @@ export class Python extends Writer { return; } - if (schema.identifier.kind === "resource") { + if (isResourceTypeSchema(schema)) { this.generateResourceTypeField(schema); } this.generateFields(schema, schema.identifier.name); - if (schema.identifier.kind === "resource") { + if (isResourceTypeSchema(schema)) { this.generateResourceMethods(schema); } } diff --git a/src/api/writer-generator/typescript/writer.ts b/src/api/writer-generator/typescript/writer.ts index 12612317c..9f5aaf073 100644 --- a/src/api/writer-generator/typescript/writer.ts +++ b/src/api/writer-generator/typescript/writer.ts @@ -324,8 +324,8 @@ export class TypeScript extends Writer { generateProfileClass(this, tsIndex, flatProfile); }); }); - } else if (["complex-type", "resource", "logical"].includes(schema.identifier.kind)) { - const resourceSchema = schema as SpecializationTypeSchema; + } else if (isSpecializationTypeSchema(schema)) { + const resourceSchema = schema; this.cat(`${tsModuleFileName(schema.identifier)}`, () => { this.generateDisclaimer(); this.generateDependenciesImports(tsIndex, resourceSchema); diff --git a/src/typeschema/ir/logic-promotion.ts b/src/typeschema/ir/logic-promotion.ts index f0a7184e3..033190bbf 100644 --- a/src/typeschema/ir/logic-promotion.ts +++ b/src/typeschema/ir/logic-promotion.ts @@ -3,6 +3,7 @@ import { type Field, type Identifier, isChoiceDeclarationField, + isLogicalTypeSchema, isPrimitiveTypeSchema, isProfileTypeSchema, isSpecializationTypeSchema, @@ -25,7 +26,7 @@ export const promoteLogical = (tsIndex: TypeSchemaIndex, promotes: LogicalPromot .map((schema) => { const promo = promoteSets[schema.identifier.package]?.has(schema.identifier.url); if (!promo) return undefined; - if (schema.identifier.kind !== "logical") + if (!isLogicalTypeSchema(schema)) throw new Error(`Unexpected schema kind: ${JSON.stringify(schema.identifier)}`); return [identifierToString(schema.identifier), { ...schema.identifier, kind: "resource" }] as const; }) From d44ea48c741c4f11883edd296d93efcb1e860a62 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 16:37:31 +0100 Subject: [PATCH 3/9] ref: Remove unsafe casts, use type predicates and generics - Replace schema.identifier.kind checks with isResourceTypeSchema, isSpecializationTypeSchema, isLogicalTypeSchema predicates - Make groupByPackages generic to preserve input type - Widen mutableSelectFields/mutableIgnoreFields to accept ProfileTypeSchema - Remove all as unknown as SpecializationTypeSchema/TypeSchema casts (NestedTypeSchema is structurally assignable to SpecializationTypeSchema) --- src/api/writer-generator/python.ts | 7 ++----- src/typeschema/ir/tree-shake.ts | 15 +++++++-------- src/typeschema/utils.ts | 8 ++++---- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/api/writer-generator/python.ts b/src/api/writer-generator/python.ts index 3fa79ae44..3357af693 100644 --- a/src/api/writer-generator/python.ts +++ b/src/api/writer-generator/python.ts @@ -173,11 +173,8 @@ export class Python extends Writer { override async generate(tsIndex: TypeSchemaIndex): Promise { this.tsIndex = tsIndex; const groups: TypeSchemaPackageGroups = { - groupedComplexTypes: groupByPackages(tsIndex.collectComplexTypes()) as Record< - string, - SpecializationTypeSchema[] - >, - groupedResources: groupByPackages(tsIndex.collectResources()) as Record, + groupedComplexTypes: groupByPackages(tsIndex.collectComplexTypes()), + groupedResources: groupByPackages(tsIndex.collectResources()), }; this.generateRootPackages(groups); this.generateSDKPackages(groups); diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index a53867e0d..e3b3b767d 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -15,7 +15,6 @@ import { isProfileTypeSchema, isSpecializationTypeSchema, isValueSetTypeSchema, - type NestedTypeSchema, type PkgName, type ProfileTypeSchema, type SpecializationTypeSchema, @@ -78,7 +77,7 @@ export const packageTreeShakeReadme = (report: TypeSchemaIndex | IrReport, pkgNa return lines.join("\n"); }; -const mutableSelectFields = (schema: SpecializationTypeSchema, selectFields: string[]) => { +const mutableSelectFields = (schema: SpecializationTypeSchema | ProfileTypeSchema, selectFields: string[]) => { const selectedFields: Record = {}; const selectPolimorphic: Record = {}; @@ -113,7 +112,7 @@ const mutableSelectFields = (schema: SpecializationTypeSchema, selectFields: str schema.fields = selectedFields; }; -const mutableIgnoreFields = (schema: SpecializationTypeSchema, ignoreFields: string[]) => { +const mutableIgnoreFields = (schema: SpecializationTypeSchema | ProfileTypeSchema, ignoreFields: string[]) => { for (const fieldName of ignoreFields) { const field = schema.fields?.[fieldName]; if (!schema.fields || !field) throw new Error(`Field ${fieldName} not found`); @@ -193,12 +192,12 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo if (rule.selectFields) { if (rule.ignoreFields) throw new Error("Cannot use both ignoreFields and selectFields in the same rule"); - mutableSelectFields(schema as SpecializationTypeSchema, rule.selectFields); + mutableSelectFields(schema, rule.selectFields); } if (rule.ignoreFields) { if (rule.selectFields) throw new Error("Cannot use both ignoreFields and selectFields in the same rule"); - mutableIgnoreFields(schema as SpecializationTypeSchema, rule.ignoreFields); + mutableIgnoreFields(schema, rule.ignoreFields); } if (isProfileTypeSchema(schema) && rule.ignoreExtensions) { @@ -207,7 +206,7 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo if (schema.nested) { const usedTypes = new Set(); - const collectUsedNestedTypes = (s: SpecializationTypeSchema | NestedTypeSchema) => { + const collectUsedNestedTypes = (s: { fields?: Record }) => { Object.values(s.fields ?? {}) .filter(isNotChoiceDeclarationField) .filter((f) => isNestedIdentifier(f.type)) @@ -221,7 +220,7 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo } }); }; - collectUsedNestedTypes(schema as SpecializationTypeSchema); + collectUsedNestedTypes(schema); schema.nested = schema.nested.filter((n) => usedTypes.has(n.identifier.url)); } @@ -271,7 +270,7 @@ export const treeShake = (tsIndex: TypeSchemaIndex, treeShake: TreeShakeConf): T for (const nest of schema.nested) { if (isNestedIdentifier(nest.identifier)) continue; const id = JSON.stringify(nest.identifier); - if (!acc[id]) newSchemas.push(nest as unknown as TypeSchema); + if (!acc[id]) newSchemas.push(nest); } } } diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index 50c9dd5c8..c8c49a4bd 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -29,15 +29,15 @@ import { /////////////////////////////////////////////////////////// // TypeSchema processing -export const groupByPackages = (typeSchemas: TypeSchema[]): Record => { - const grouped = {} as Record; +export const groupByPackages = (typeSchemas: T[]): Record => { + const grouped = {} as Record; for (const ts of typeSchemas) { const pkgName = ts.identifier.package; if (!grouped[pkgName]) grouped[pkgName] = []; grouped[pkgName].push(ts); } for (const [packageName, typeSchemas] of Object.entries(grouped)) { - const dict: Record = {}; + const dict: Record = {}; for (const ts of typeSchemas) { dict[JSON.stringify(ts.identifier)] = ts; } @@ -208,7 +208,7 @@ export const mkTypeSchemaIndex = ( const nurl = nschema.identifier.url; const npkg = nschema.identifier.package; nestedIndex[nurl] ??= {}; - nestedIndex[nurl][npkg] = nschema as unknown as TypeSchema; + nestedIndex[nurl][npkg] = nschema; }); } } From 58f8d2804ef76095f1fee1ba1b06031531b5fb07 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 16:45:24 +0100 Subject: [PATCH 4/9] ref: Use const arrow style for collectRawDeps, extractDependencies, extractProfileDependencies --- src/typeschema/core/transformer.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 3056bf53c..a217c5c94 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -93,24 +93,24 @@ export async function transformValueSet( }; } -function collectRawDeps( +const collectRawDeps = ( base: TypeIdentifier | undefined, fields: Record | undefined, nestedTypes: NestedTypeSchema[] | undefined, -): TypeIdentifier[] { +): TypeIdentifier[] => { const deps: TypeIdentifier[] = []; if (base) deps.push(base); if (fields) deps.push(...extractFieldDependencies(fields)); if (nestedTypes) deps.push(...extractNestedDependencies(nestedTypes)); return deps; -} +}; -export function extractDependencies( +export const extractDependencies = ( identifier: TypeIdentifier, base: TypeIdentifier | undefined, fields: Record | undefined, nestedTypes: NestedTypeSchema[] | undefined, -): Identifier[] | undefined { +): Identifier[] | undefined => { const deps = collectRawDeps(base, fields, nestedTypes); const filtered = deps.filter((dep): dep is Identifier => { @@ -120,20 +120,20 @@ export function extractDependencies( }); return concatIdentifiers(filtered) as Identifier[] | undefined; -} +}; -export function extractProfileDependencies( +export const extractProfileDependencies = ( identifier: TypeIdentifier, base: TypeIdentifier | undefined, fields: Record | undefined, nestedTypes: NestedTypeSchema[] | undefined, -): TypeIdentifier[] | undefined { +): TypeIdentifier[] | undefined => { const deps = collectRawDeps(base, fields, nestedTypes); const filtered = deps.filter((dep) => dep.url !== identifier.url); return concatIdentifiers(filtered); -} +}; export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSchema, logger?: CodegenLog): TypeSchema[] { const identifier = mkIdentifier(fhirSchema); From aa87bcdd390ad27fab516d707accaceb68f8d35e Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 16:47:01 +0100 Subject: [PATCH 5/9] ref: Remove as TypeSchema cast, use typed local variable --- src/typeschema/core/transformer.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index a217c5c94..a0b1fa596 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -19,6 +19,7 @@ import { packageMetaToFhir, type RichFHIRSchema, type RichValueSet, + type SpecializationTypeSchema, type TypeIdentifier, type TypeSchema, type ValueSetTypeSchema, @@ -171,7 +172,7 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche }; } else { const rawDeps = extractDependencies(identifier, base, fields, nested); - typeSchema = { + const specialization: SpecializationTypeSchema = { identifier, base, fields, @@ -179,7 +180,8 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche description: fhirSchema.description, dependencies: rawDeps, typeFamily: undefined, // NOTE: should be populateTypeFamily later. - } as TypeSchema; + }; + typeSchema = specialization; } const bindingSchemas = collectBindingSchemas(register, fhirSchema, logger); From a16e7be7112c928868028e2b89a87e21bead547a Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 16:50:03 +0100 Subject: [PATCH 6/9] ref: Replace as ProfileIdentifier cast with assert --- src/typeschema/core/transformer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index a0b1fa596..136d44254 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -4,6 +4,7 @@ * Core transformation logic for converting FHIRSchema to TypeSchema format */ +import assert from "node:assert"; import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema"; import { shouldSkipCanonical } from "@root/typeschema/skip-hack"; import type { CodegenLog } from "@root/utils/log"; @@ -14,8 +15,8 @@ import { type Field, type Identifier, isNestedIdentifier, + isProfileIdentifier, type NestedTypeSchema, - type ProfileIdentifier, packageMetaToFhir, type RichFHIRSchema, type RichValueSet, @@ -158,11 +159,15 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche let typeSchema: TypeSchema; if (fhirSchema.derivation === "constraint") { if (!base) throw new Error(`Profile ${fhirSchema.url} must have a base type`); + assert( + isProfileIdentifier(identifier), + `Expected profile identifier for ${fhirSchema.url}, got ${identifier.kind}`, + ); const extensions = extractProfileExtensions(register, fhirSchema, logger); const extensionDeps = extensions?.flatMap(extractExtensionDeps); const rawDeps = extractProfileDependencies(identifier, base, fields, nested); typeSchema = { - identifier: identifier as ProfileIdentifier, + identifier, base, fields, nested, From e67d538bff00085f53d27807215a210af53fe7de Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 16:51:40 +0100 Subject: [PATCH 7/9] ref: Narrow extractProfileDependencies to accept ProfileIdentifier --- src/typeschema/core/transformer.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 136d44254..8c2532462 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -17,6 +17,7 @@ import { isNestedIdentifier, isProfileIdentifier, type NestedTypeSchema, + type ProfileIdentifier, packageMetaToFhir, type RichFHIRSchema, type RichValueSet, @@ -125,15 +126,13 @@ export const extractDependencies = ( }; export const extractProfileDependencies = ( - identifier: TypeIdentifier, + identifier: ProfileIdentifier, base: TypeIdentifier | undefined, fields: Record | undefined, nestedTypes: NestedTypeSchema[] | undefined, ): TypeIdentifier[] | undefined => { const deps = collectRawDeps(base, fields, nestedTypes); - const filtered = deps.filter((dep) => dep.url !== identifier.url); - return concatIdentifiers(filtered); }; From aaf5e2d100d09de5b7de02c63908824fd650e64b Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 16:52:59 +0100 Subject: [PATCH 8/9] ref: Narrow extractDependencies to accept Identifier --- src/typeschema/core/transformer.ts | 3 ++- src/typeschema/ir/tree-shake.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 8c2532462..4da5f46cd 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -109,7 +109,7 @@ const collectRawDeps = ( }; export const extractDependencies = ( - identifier: TypeIdentifier, + identifier: Identifier, base: TypeIdentifier | undefined, fields: Record | undefined, nestedTypes: NestedTypeSchema[] | undefined, @@ -175,6 +175,7 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche extensions, }; } else { + assert(!isNestedIdentifier(identifier), `Unexpected nested identifier for ${fhirSchema.url}`); const rawDeps = extractDependencies(identifier, base, fields, nested); const specialization: SpecializationTypeSchema = { identifier, diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index e3b3b767d..b979045d9 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -231,6 +231,7 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo extDeps, ); } else { + assert(!isNestedIdentifier(schema.identifier)); schema.dependencies = extractDependencies(schema.identifier, schema.base, schema.fields, schema.nested); } return schema; From 245139df01957da168939eb09c07e3fba82c16e0 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Tue, 24 Mar 2026 00:44:20 +0100 Subject: [PATCH 9/9] ref: Make concatIdentifiers generic, narrow base to Identifier --- src/api/writer-generator/typescript/writer.ts | 11 +++++------ src/typeschema/core/transformer.ts | 13 ++++++------- src/typeschema/types.ts | 10 ++++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/api/writer-generator/typescript/writer.ts b/src/api/writer-generator/typescript/writer.ts index 9f5aaf073..655c55453 100644 --- a/src/api/writer-generator/typescript/writer.ts +++ b/src/api/writer-generator/typescript/writer.ts @@ -325,19 +325,18 @@ export class TypeScript extends Writer { }); }); } else if (isSpecializationTypeSchema(schema)) { - const resourceSchema = schema; this.cat(`${tsModuleFileName(schema.identifier)}`, () => { this.generateDisclaimer(); - this.generateDependenciesImports(tsIndex, resourceSchema); - this.generateComplexTypeReexports(resourceSchema); - this.generateNestedTypes(tsIndex, resourceSchema); + this.generateDependenciesImports(tsIndex, schema); + this.generateComplexTypeReexports(schema); + this.generateNestedTypes(tsIndex, schema); this.comment( "CanonicalURL:", schema.identifier.url, `(pkg: ${packageMetaToFhir(packageMeta(schema))})`, ); - this.generateType(tsIndex, resourceSchema); - this.generateResourceTypePredicate(resourceSchema); + this.generateType(tsIndex, schema); + this.generateResourceTypePredicate(schema); }); } else { throw new Error(`Profile generation not implemented for kind: ${schema.identifier.kind}`); diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 4da5f46cd..9c37fb43f 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -122,7 +122,7 @@ export const extractDependencies = ( return true; }); - return concatIdentifiers(filtered) as Identifier[] | undefined; + return concatIdentifiers(filtered); }; export const extractProfileDependencies = ( @@ -139,7 +139,7 @@ export const extractProfileDependencies = ( export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSchema, logger?: CodegenLog): TypeSchema[] { const identifier = mkIdentifier(fhirSchema); - let base: TypeIdentifier | undefined; + let base: Identifier | undefined; if (fhirSchema.base) { const baseFs = register.resolveFs( fhirSchema.package_meta, @@ -149,7 +149,9 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche throw new Error( `Base resource not found '${fhirSchema.base}' for <${fhirSchema.url}> from ${packageMetaToFhir(fhirSchema.package_meta)}`, ); - base = mkIdentifier(baseFs); + const baseId = mkIdentifier(baseFs); + assert(!isNestedIdentifier(baseId), `Unexpected nested base for ${fhirSchema.url}`); + base = baseId; } const fields = mkFields(register, fhirSchema, [], fhirSchema.elements, logger); @@ -158,10 +160,7 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche let typeSchema: TypeSchema; if (fhirSchema.derivation === "constraint") { if (!base) throw new Error(`Profile ${fhirSchema.url} must have a base type`); - assert( - isProfileIdentifier(identifier), - `Expected profile identifier for ${fhirSchema.url}, got ${identifier.kind}`, - ); + assert(isProfileIdentifier(identifier)); const extensions = extractProfileExtensions(register, fhirSchema, logger); const extensionDeps = extensions?.flatMap(extractExtensionDeps); const rawDeps = extractProfileDependencies(identifier, base, fields, nested); diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index e27867a4d..264fbd35d 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -137,12 +137,14 @@ export const isProfileIdentifier = (id: TypeIdentifier | undefined): id is Profi return id?.kind === "profile"; }; -export const concatIdentifiers = (...sources: (TypeIdentifier[] | undefined)[]): TypeIdentifier[] | undefined => { +export const concatIdentifiers = ( + ...sources: (T[] | undefined)[] +): T[] | undefined => { const entries = sources - .filter((s): s is TypeIdentifier[] => s !== undefined) - .flatMap((s) => s.map((id): [string, TypeIdentifier] => [id.url, id])); + .filter((s): s is T[] => s !== undefined) + .flatMap((s) => s.map((id): [string, T] => [id.url, id])); if (entries.length === 0) return undefined; - const deduped = Object.values(Object.fromEntries(entries) as Record); + const deduped = Object.values(Object.fromEntries(entries) as Record); return deduped.sort((a, b) => a.url.localeCompare(b.url)); };