typedapi-client-helpers 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,20 @@ function intersectionTypes(types, fallback = "Record<string, unknown>") {
42
42
  return unique.length > 0 ? unique.join(" & ") : fallback;
43
43
  }
44
44
 
45
+ function escapeRegExp(value) {
46
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ }
48
+
49
+ function genericDeclarationBody(declaration) {
50
+ if (declaration.startsWith("export interface ")) {
51
+ const bodyStart = declaration.indexOf("{");
52
+ return bodyStart >= 0 ? declaration.slice(bodyStart + 1) : "";
53
+ }
54
+
55
+ const bodyStart = declaration.indexOf("=");
56
+ return bodyStart >= 0 ? declaration.slice(bodyStart + 1) : "";
57
+ }
58
+
45
59
  function hasObjectShape(schema) {
46
60
  return Boolean(
47
61
  schema?.properties ||
@@ -55,6 +69,170 @@ function schemaWithoutComposition(schema) {
55
69
  return local;
56
70
  }
57
71
 
72
+
73
+ function normalizeGenericBindings(bindings, parameters, contextName) {
74
+ const normalized = Array.isArray(bindings)
75
+ ? bindings
76
+ .map((binding) => ({
77
+ parameter: String(binding?.parameter ?? "").trim(),
78
+ path: String(binding?.path ?? "").trim(),
79
+ }))
80
+ .filter((binding) => binding.parameter && binding.path.startsWith("/"))
81
+ : [];
82
+ for (const binding of normalized) {
83
+ if (!parameters.includes(binding.parameter)) {
84
+ throw new Error(`${contextName} binding ${binding.path} refers to unknown parameter ${binding.parameter}.`);
85
+ }
86
+ }
87
+ return normalized;
88
+ }
89
+
90
+ function normalizeGenericArgument(argument, parameters, contextName) {
91
+ if (!argument || typeof argument !== "object") {
92
+ throw new Error(`${contextName} contains an invalid generic argument.`);
93
+ }
94
+
95
+ const genericParameter = String(argument.genericParameter ?? "").trim();
96
+ const primitive = String(argument.primitive ?? "").trim();
97
+ const schemaId = String(argument.schemaId ?? "").trim();
98
+ const populated = [genericParameter, primitive, schemaId].filter(Boolean);
99
+ if (populated.length !== 1) {
100
+ throw new Error(`${contextName} generic arguments must specify exactly one of genericParameter, primitive, or schemaId.`);
101
+ }
102
+ if (genericParameter && !parameters.includes(genericParameter)) {
103
+ throw new Error(`${contextName} refers to unknown generic parameter ${genericParameter}.`);
104
+ }
105
+ if (genericParameter) return { genericParameter };
106
+ if (primitive) return { primitive };
107
+ return { schemaId };
108
+ }
109
+
110
+ function normalizeGenericBase(base, ownerParameters, contextName) {
111
+ if (!base || typeof base !== "object") return undefined;
112
+ const definition = String(base.definition ?? "").trim();
113
+ const parameters = Array.isArray(base.parameters)
114
+ ? base.parameters.map((item) => String(item)).filter(Boolean)
115
+ : [];
116
+ const rawArguments = Array.isArray(base.arguments) ? base.arguments : [];
117
+ const properties = Array.isArray(base.properties)
118
+ ? [...new Set(base.properties.map((item) => String(item)).filter(Boolean))]
119
+ : [];
120
+ if (!definition || parameters.length === 0 || parameters.length !== rawArguments.length) {
121
+ throw new Error(`Invalid generic base metadata for ${contextName}.`);
122
+ }
123
+ const argumentsList = rawArguments.map((argument) =>
124
+ normalizeGenericArgument(argument, ownerParameters, `Generic base ${definition}`),
125
+ );
126
+ const bindings = normalizeGenericBindings(
127
+ base.bindings,
128
+ parameters,
129
+ `Generic base ${definition}`,
130
+ );
131
+ return {
132
+ definition,
133
+ parameters,
134
+ arguments: argumentsList,
135
+ properties,
136
+ bindings,
137
+ base: normalizeGenericBase(base.base, parameters, definition),
138
+ };
139
+ }
140
+
141
+ function genericMetadata(schema) {
142
+ const metadata = schema?.["x-typedapi-generic"];
143
+ if (!metadata || typeof metadata !== "object") return undefined;
144
+ const definition = String(metadata.definition ?? "").trim();
145
+ const parameters = Array.isArray(metadata.parameters)
146
+ ? metadata.parameters.map((item) => String(item)).filter(Boolean)
147
+ : [];
148
+ const argumentsList = Array.isArray(metadata.arguments) ? metadata.arguments : [];
149
+ if (!definition || parameters.length === 0 || parameters.length !== argumentsList.length) {
150
+ throw new Error(`Invalid x-typedapi-generic metadata for ${definition || "an unnamed schema"}.`);
151
+ }
152
+ const bindings = normalizeGenericBindings(metadata.bindings, parameters, `Generic schema ${definition}`);
153
+ return {
154
+ definition,
155
+ parameters,
156
+ arguments: argumentsList,
157
+ bindings,
158
+ base: normalizeGenericBase(metadata.base, parameters, definition),
159
+ };
160
+ }
161
+
162
+ function decodeJsonPointerPart(value) {
163
+ return value.replace(/~1/g, "/").replace(/~0/g, "~");
164
+ }
165
+
166
+ function cloneSchemaWithGenericBindings(schema, metadata) {
167
+ const clone = structuredClone(schema);
168
+ for (const binding of metadata.bindings ?? []) {
169
+ const parts = binding.path.split("/").slice(1).map(decodeJsonPointerPart);
170
+ if (parts.length === 0) continue;
171
+ let parent = clone;
172
+ for (const part of parts.slice(0, -1)) {
173
+ parent = parent?.[part];
174
+ if (!parent || typeof parent !== "object") break;
175
+ }
176
+ if (!parent || typeof parent !== "object") continue;
177
+ const finalPart = parts.at(-1);
178
+ if (!Object.prototype.hasOwnProperty.call(parent, finalPart)) continue;
179
+ parent[finalPart] = { "x-typedapi-generic-parameter": binding.parameter };
180
+ }
181
+ return clone;
182
+ }
183
+
184
+ function copySelectedProperties(schema, selectedProperties) {
185
+ const selected = new Set(selectedProperties ?? []);
186
+ const properties = Object.fromEntries(
187
+ Object.entries(schema?.properties ?? {}).filter(([name]) => selected.has(name)),
188
+ );
189
+ const required = (schema?.required ?? []).filter((name) => selected.has(name));
190
+ const result = {
191
+ ...schemaWithoutComposition(schema),
192
+ type: "object",
193
+ properties,
194
+ };
195
+ delete result["x-typedapi-generic"];
196
+ if (required.length > 0) result.required = required;
197
+ else delete result.required;
198
+ return result;
199
+ }
200
+
201
+ function removeSelectedProperties(schema, selectedProperties) {
202
+ const selected = new Set(selectedProperties ?? []);
203
+ const result = structuredClone(schemaWithoutComposition(schema));
204
+ result.properties = Object.fromEntries(
205
+ Object.entries(result.properties ?? {}).filter(([name]) => !selected.has(name)),
206
+ );
207
+ result.required = (result.required ?? []).filter((name) => !selected.has(name));
208
+ delete result["x-typedapi-generic"];
209
+ if (Object.keys(result.properties).length === 0) delete result.properties;
210
+ if (result.required.length === 0) delete result.required;
211
+ return result;
212
+ }
213
+
214
+ function hasOwnObjectMembers(schema) {
215
+ return Object.keys(schema?.properties ?? {}).length > 0 || Boolean(schema?.additionalProperties);
216
+ }
217
+
218
+ function discriminatorMappings(schema) {
219
+ const discriminator = schema?.discriminator;
220
+ if (!discriminator?.propertyName) return [];
221
+ const mapping = discriminator.mapping ?? {};
222
+ const entries = Object.entries(mapping)
223
+ .filter(([, ref]) => typeof ref === "string" && ref.includes("/"))
224
+ .map(([value, ref]) => ({ propertyName: discriminator.propertyName, value, rawName: refName(ref) }));
225
+ if (entries.length > 0) return entries;
226
+
227
+ return (schema.oneOf ?? [])
228
+ .filter((item) => item?.$ref)
229
+ .map((item) => ({
230
+ propertyName: discriminator.propertyName,
231
+ value: refName(item.$ref),
232
+ rawName: refName(item.$ref),
233
+ }));
234
+ }
235
+
58
236
  function assertUniqueTypePropertyNames(entries, contextName) {
59
237
  const seen = new Map();
60
238
  for (const rawName of entries) {
@@ -77,24 +255,149 @@ export class TypeResolver {
77
255
  this.options = options;
78
256
  this.inlineTypes = new Map();
79
257
  this.componentNames = new Map();
258
+ this.genericSchemas = new Map();
259
+ this.genericDefinitions = new Map();
260
+ this.discriminatorProperties = new Map();
261
+ this.polymorphicBases = new Map();
262
+ const schemas = openApi.components?.schemas ?? {};
80
263
  const usedNames = new Set();
81
264
 
82
- for (const rawName of Object.keys(openApi.components?.schemas ?? {})) {
265
+ const registerGenericDefinition = (normalized) => {
266
+ const existing = this.genericDefinitions.get(normalized.definitionName);
267
+ if (existing && existing.parameters.join("|") !== normalized.parameters.join("|")) {
268
+ throw new Error(`Generic schemas for ${normalized.definitionName} use inconsistent type parameters.`);
269
+ }
270
+ if (existing && JSON.stringify(existing.bindings) !== JSON.stringify(normalized.bindings)) {
271
+ throw new Error(`Generic schemas for ${normalized.definitionName} use inconsistent generic bindings.`);
272
+ }
273
+ if (existing && JSON.stringify(existing.base ?? null) !== JSON.stringify(normalized.base ?? null)) {
274
+ throw new Error(`Generic schemas for ${normalized.definitionName} use inconsistent generic inheritance metadata.`);
275
+ }
276
+ if (!existing || (existing.synthetic && !normalized.synthetic)) {
277
+ this.genericDefinitions.set(normalized.definitionName, normalized);
278
+ }
279
+ usedNames.add(normalized.definitionName);
280
+ };
281
+
282
+ for (const [rawName, schema] of Object.entries(schemas)) {
283
+ const metadata = genericMetadata(schema);
284
+ if (!metadata) continue;
285
+ const definitionName = operationTypeName(metadata.definition);
286
+ const normalized = { ...metadata, definitionName, rawName, synthetic: false };
287
+ this.genericSchemas.set(rawName, normalized);
288
+ registerGenericDefinition(normalized);
289
+ }
290
+
291
+ const resolveConcreteBaseArguments = (base, ownerMetadata) => {
292
+ const ownerArguments = new Map(
293
+ ownerMetadata.parameters.map((parameter, index) => [parameter, ownerMetadata.arguments[index]]),
294
+ );
295
+ return base.arguments.map((argument, index) => {
296
+ const concrete = argument.genericParameter
297
+ ? ownerArguments.get(argument.genericParameter)
298
+ : argument;
299
+ if (!concrete) {
300
+ throw new Error(
301
+ `Generic base ${base.definition} could not resolve argument ${base.parameters[index]}.`,
302
+ );
303
+ }
304
+ const { parameter: _ignoredParameter, ...resolvedArgument } = concrete;
305
+ return { parameter: base.parameters[index], ...resolvedArgument };
306
+ });
307
+ };
308
+
309
+ const registerSyntheticBases = (ownerMetadata, sourceRawName) => {
310
+ const base = ownerMetadata.base;
311
+ if (!base) return;
312
+ const definitionName = operationTypeName(base.definition);
313
+ const normalized = {
314
+ definition: base.definition,
315
+ definitionName,
316
+ parameters: base.parameters,
317
+ arguments: resolveConcreteBaseArguments(base, ownerMetadata),
318
+ bindings: base.bindings,
319
+ base: base.base,
320
+ rawName: sourceRawName,
321
+ synthetic: true,
322
+ syntheticProperties: base.properties,
323
+ };
324
+ registerGenericDefinition(normalized);
325
+ registerSyntheticBases(normalized, sourceRawName);
326
+ };
327
+
328
+ for (const metadata of this.genericSchemas.values()) {
329
+ registerSyntheticBases(metadata, metadata.rawName);
330
+ }
331
+
332
+ for (const rawName of Object.keys(schemas)) {
333
+ if (this.genericSchemas.has(rawName)) continue;
83
334
  const name = uniqueName(operationTypeName(rawName), usedNames);
84
335
  this.componentNames.set(rawName, name);
85
336
  }
337
+
338
+ for (const [rawBaseName, schema] of Object.entries(schemas)) {
339
+ const mappings = discriminatorMappings(schema);
340
+ if (mappings.length > 0) this.polymorphicBases.set(rawBaseName, mappings);
341
+ for (const mapping of mappings) {
342
+ const properties = this.discriminatorProperties.get(mapping.rawName) ?? new Map();
343
+ const previous = properties.get(mapping.propertyName);
344
+ if (previous !== undefined && previous !== mapping.value) {
345
+ throw new Error(
346
+ `Schema ${mapping.rawName} has conflicting discriminator values for ${mapping.propertyName}.`,
347
+ );
348
+ }
349
+ properties.set(mapping.propertyName, mapping.value);
350
+ this.discriminatorProperties.set(mapping.rawName, properties);
351
+ }
352
+ }
86
353
  }
87
354
 
88
355
  componentName(rawName) {
89
356
  return this.componentNames.get(rawName) ?? operationTypeName(rawName);
90
357
  }
91
358
 
359
+ componentType(rawName, options = {}) {
360
+ if (options.asInheritanceBase && this.polymorphicBases.has(rawName)) {
361
+ return `${this.componentName(rawName)}Base`;
362
+ }
363
+
364
+ const metadata = this.genericSchemas.get(rawName);
365
+ if (!metadata) return this.componentName(rawName);
366
+
367
+ const argumentTypes = metadata.arguments.map((argument) => {
368
+ if (argument?.primitive) return String(argument.primitive);
369
+ if (argument?.schemaId) {
370
+ const schemaId = String(argument.schemaId);
371
+ return options.genericArgumentSubstitutions?.get(schemaId)
372
+ ?? this.componentType(schemaId, options);
373
+ }
374
+ return "unknown";
375
+ });
376
+ return `${metadata.definitionName}<${argumentTypes.join(", ")}>`;
377
+ }
378
+
379
+ genericBaseType(base, ownerParameters) {
380
+ const argumentTypes = base.arguments.map((argument) => {
381
+ if (argument.genericParameter) return argument.genericParameter;
382
+ if (argument.primitive) return argument.primitive;
383
+ if (argument.schemaId) return this.componentType(argument.schemaId);
384
+ return "unknown";
385
+ });
386
+ return `${operationTypeName(base.definition)}<${argumentTypes.join(", ")}>`;
387
+ }
388
+
389
+ isGenericBaseReference(item, base) {
390
+ if (!item?.$ref) return false;
391
+ const metadata = this.genericSchemas.get(refName(item.$ref));
392
+ return metadata?.definitionName === operationTypeName(base.definition);
393
+ }
394
+
92
395
  resolve(schemaOrRef, contextName = "Anonymous", options = {}) {
93
396
  if (!schemaOrRef) return "unknown";
94
397
 
95
398
  if (schemaOrRef.$ref) {
96
399
  dereference(this.openApi, schemaOrRef, "Schema reference");
97
- return this.componentName(refName(schemaOrRef.$ref));
400
+ return this.componentType(refName(schemaOrRef.$ref), options);
98
401
  }
99
402
 
100
403
  const schema =
@@ -108,6 +411,8 @@ export class TypeResolver {
108
411
  }
109
412
 
110
413
  resolveNonNullable(schema, contextName, options = {}) {
414
+ const genericParameter = schema?.["x-typedapi-generic-parameter"];
415
+ if (genericParameter) return String(genericParameter);
111
416
  if (schema.const !== undefined) return JSON.stringify(schema.const);
112
417
 
113
418
  if (Array.isArray(schema.enum)) {
@@ -130,7 +435,10 @@ export class TypeResolver {
130
435
 
131
436
  if (schema.allOf) {
132
437
  const parts = schema.allOf.map((item, index) =>
133
- this.resolve(item, `${contextName}${index + 1}`, options),
438
+ this.resolve(item, `${contextName}${index + 1}`, {
439
+ ...options,
440
+ asInheritanceBase: true,
441
+ }),
134
442
  );
135
443
  const localSchema = schemaWithoutComposition(schema);
136
444
  if (hasObjectShape(localSchema)) {
@@ -189,17 +497,18 @@ export class TypeResolver {
189
497
  }
190
498
  }
191
499
 
192
- createInlineObjectType(schema, contextName) {
500
+ createInlineObjectType(schema, contextName, options = {}) {
193
501
  const base = operationTypeName(contextName);
194
502
  let name = base;
195
503
  let counter = 2;
196
504
  while (
197
505
  this.componentNamesHasValue(name) ||
198
- (this.inlineTypes.has(name) && this.inlineTypes.get(name) !== schema)
506
+ this.genericDefinitions.has(name) ||
507
+ (this.inlineTypes.has(name) && this.inlineTypes.get(name).schema !== schema)
199
508
  ) {
200
509
  name = `${base}${counter++}`;
201
510
  }
202
- if (!this.inlineTypes.has(name)) this.inlineTypes.set(name, schema);
511
+ if (!this.inlineTypes.has(name)) this.inlineTypes.set(name, { schema, options });
203
512
  return name;
204
513
  }
205
514
 
@@ -207,26 +516,29 @@ export class TypeResolver {
207
516
  return [...this.componentNames.values()].includes(value);
208
517
  }
209
518
 
210
- objectMembers(name, schema) {
519
+ objectMembers(name, schema, options = {}) {
211
520
  const required = new Set(schema.required ?? []);
212
521
  const lines = [];
213
522
 
214
523
  const propertyEntries = Object.entries(schema.properties ?? {});
524
+ const syntheticDiscriminators = options.rawName
525
+ ? this.discriminatorProperties.get(options.rawName) ?? new Map()
526
+ : new Map();
215
527
  assertUniqueTypePropertyNames(
216
- propertyEntries.map(([propertyName]) => propertyName),
528
+ [...propertyEntries.map(([propertyName]) => propertyName), ...syntheticDiscriminators.keys()],
217
529
  name,
218
530
  );
219
531
 
220
532
  for (const [propertyName, propertySchema] of propertyEntries) {
221
533
  const localPropertyName = typePropertyName(propertyName);
222
- const optionalToken = required.has(propertyName) ? "" : "?";
223
- const propertyType = this.resolve(
224
- propertySchema,
225
- `${name}${pascalCase(propertyName)}`,
226
- {
227
- inlineEnumAsUnion: true,
228
- },
229
- );
534
+ const discriminatorValue = syntheticDiscriminators.get(propertyName);
535
+ const optionalToken = discriminatorValue !== undefined || required.has(propertyName) ? "" : "?";
536
+ const propertyType = discriminatorValue !== undefined
537
+ ? JSON.stringify(discriminatorValue)
538
+ : this.resolve(propertySchema, `${name}${pascalCase(propertyName)}`, {
539
+ ...options,
540
+ inlineEnumAsUnion: true,
541
+ });
230
542
  const resolvedSchema = dereference(
231
543
  this.openApi,
232
544
  propertySchema,
@@ -238,11 +550,17 @@ export class TypeResolver {
238
550
  );
239
551
  }
240
552
 
553
+ for (const [propertyName, value] of syntheticDiscriminators) {
554
+ if (Object.prototype.hasOwnProperty.call(schema.properties ?? {}, propertyName)) continue;
555
+ lines.push(` ${propertyKey(typePropertyName(propertyName))}: ${JSON.stringify(value)};`);
556
+ }
557
+
241
558
  if (schema.additionalProperties) {
242
559
  const valueType =
243
560
  schema.additionalProperties === true
244
561
  ? "unknown"
245
562
  : this.resolve(schema.additionalProperties, `${name}Value`, {
563
+ ...options,
246
564
  inlineEnumAsUnion: true,
247
565
  });
248
566
  lines.push(` [key: string]: ${valueType};`);
@@ -251,33 +569,133 @@ export class TypeResolver {
251
569
  return lines;
252
570
  }
253
571
 
254
- createInterface(name, schema) {
572
+ createInterface(name, schema, options = {}) {
255
573
  return [
256
574
  `export interface ${name} {`,
257
- ...this.objectMembers(name, schema),
575
+ ...this.objectMembers(name, schema, options),
258
576
  "}",
259
577
  ].join("\n");
260
578
  }
261
579
 
262
- createObjectTypeLiteral(name, schema) {
263
- return ["{", ...this.objectMembers(name, schema), "}"].join("\n");
580
+ createObjectTypeLiteral(name, schema, options = {}) {
581
+ return ["{", ...this.objectMembers(name, schema, options), "}"].join("\n");
264
582
  }
265
583
 
266
- createDeclaration(name, schema) {
584
+ createDeclaration(name, schema, options = {}) {
267
585
  if (Array.isArray(schema.enum)) return this.createEnum(name, schema);
268
586
 
269
- if (schema.allOf) {
270
- const parts = schema.allOf.map((item, index) =>
271
- this.resolve(item, `${name}${index + 1}`, { inlineEnumAsUnion: true }),
587
+ if (schema.oneOf || schema.anyOf) {
588
+ const variants = schema.oneOf ?? schema.anyOf;
589
+ const variantTypes = variants.map((item, index) =>
590
+ this.resolve(item, `${name}${index + 1}`, { ...options, inlineEnumAsUnion: true }),
272
591
  );
592
+ return `export type ${name} = ${unionTypes(variantTypes)};`;
593
+ }
594
+
595
+ if (schema.allOf) {
596
+ const parts = schema.allOf.map((item, index) => {
597
+ const resolvedItem = dereference(this.openApi, item, "allOf schema reference");
598
+ if (!item?.$ref && hasObjectShape(resolvedItem) && !resolvedItem.allOf && !resolvedItem.oneOf && !resolvedItem.anyOf) {
599
+ return this.createObjectTypeLiteral(`${name}${index + 1}`, resolvedItem, {
600
+ ...options,
601
+ rawName: undefined,
602
+ });
603
+ }
604
+ return this.resolve(item, `${name}${index + 1}`, {
605
+ ...options,
606
+ inlineEnumAsUnion: true,
607
+ asInheritanceBase: true,
608
+ });
609
+ });
273
610
  const localSchema = schemaWithoutComposition(schema);
274
- if (hasObjectShape(localSchema))
275
- parts.push(this.createObjectTypeLiteral(`${name}Own`, localSchema));
611
+ const hasSyntheticDiscriminator = options.rawName && this.discriminatorProperties.has(options.rawName);
612
+ if (hasObjectShape(localSchema) || hasSyntheticDiscriminator)
613
+ parts.push(this.createObjectTypeLiteral(`${name}Own`, localSchema, options));
276
614
  return `export type ${name} = ${intersectionTypes(parts)};`;
277
615
  }
278
616
 
279
- if (hasObjectShape(schema)) return this.createInterface(name, schema);
280
- return `export type ${name} = ${this.resolve(schema, name, { inlineEnumAsUnion: true })};`;
617
+ if (hasObjectShape(schema)) return this.createInterface(name, schema, options);
618
+ return `export type ${name} = ${this.resolve(schema, name, { ...options, inlineEnumAsUnion: true })};`;
619
+ }
620
+
621
+ createGenericDeclaration(metadata, schema) {
622
+ const substitutions = new Map();
623
+ metadata.arguments.forEach((argument, index) => {
624
+ if (argument?.schemaId) substitutions.set(String(argument.schemaId), metadata.parameters[index]);
625
+ });
626
+ const genericName = `${metadata.definitionName}<${metadata.parameters.join(", ")}>`;
627
+ const templateSchema = cloneSchemaWithGenericBindings(schema, metadata);
628
+ let declaration;
629
+
630
+ if (metadata.base) {
631
+ const inheritedProperties = new Set(metadata.base.properties ?? []);
632
+ const ownSchema = removeSelectedProperties(templateSchema, inheritedProperties);
633
+ const parts = [this.genericBaseType(metadata.base, metadata.parameters)];
634
+
635
+ for (const [index, item] of (templateSchema.allOf ?? []).entries()) {
636
+ if (this.isGenericBaseReference(item, metadata.base)) continue;
637
+ const resolvedItem = dereference(this.openApi, item, "allOf schema reference");
638
+ if (!item?.$ref && hasObjectShape(resolvedItem) && !resolvedItem.allOf && !resolvedItem.oneOf && !resolvedItem.anyOf) {
639
+ const ownInlineSchema = removeSelectedProperties(resolvedItem, inheritedProperties);
640
+ if (hasOwnObjectMembers(ownInlineSchema)) {
641
+ parts.push(this.createObjectTypeLiteral(`${genericName}${index + 1}`, ownInlineSchema, {
642
+ rawName: undefined,
643
+ genericArgumentSubstitutions: substitutions,
644
+ }));
645
+ }
646
+ continue;
647
+ }
648
+ parts.push(this.resolve(item, `${genericName}${index + 1}`, {
649
+ inlineEnumAsUnion: true,
650
+ genericArgumentSubstitutions: substitutions,
651
+ asInheritanceBase: true,
652
+ }));
653
+ }
654
+
655
+ if (hasOwnObjectMembers(ownSchema)) {
656
+ parts.push(this.createObjectTypeLiteral(`${genericName}Own`, ownSchema, {
657
+ rawName: metadata.rawName,
658
+ genericArgumentSubstitutions: substitutions,
659
+ }));
660
+ }
661
+ declaration = `export type ${genericName} = ${intersectionTypes(parts)};`;
662
+ } else {
663
+ declaration = this.createDeclaration(genericName, templateSchema, {
664
+ rawName: metadata.rawName,
665
+ genericArgumentSubstitutions: substitutions,
666
+ });
667
+ }
668
+
669
+ const body = genericDeclarationBody(declaration);
670
+ const unusedParameters = metadata.parameters.filter((parameter) =>
671
+ !new RegExp(`\\b${escapeRegExp(parameter)}\\b`).test(body),
672
+ );
673
+ if (unusedParameters.length > 0) {
674
+ throw new Error(
675
+ `Generic schema ${metadata.rawName} declares unbound type parameter${unusedParameters.length === 1 ? "" : "s"} ` +
676
+ `${unusedParameters.join(", ")}. Ensure x-typedapi-generic.bindings includes inherited or flattened generic properties.`,
677
+ );
678
+ }
679
+ return declaration;
680
+ }
681
+
682
+ isPolymorphicBase(rawName) {
683
+ return this.polymorphicBases.has(rawName);
684
+ }
685
+
686
+ createPolymorphicDeclarations(rawName, schema) {
687
+ const name = this.componentName(rawName);
688
+ const mappings = this.polymorphicBases.get(rawName) ?? [];
689
+ const variants = mappings.map((mapping) => this.componentType(mapping.rawName));
690
+ const { oneOf, anyOf, discriminator, ...baseSchema } = schema;
691
+ const declarations = [];
692
+ if (hasObjectShape(baseSchema) || baseSchema.allOf) {
693
+ declarations.push(this.createDeclaration(`${name}Base`, baseSchema));
694
+ } else {
695
+ declarations.push(`export interface ${name}Base {}`);
696
+ }
697
+ declarations.push(`export type ${name} = ${unionTypes(variants)};`);
698
+ return declarations;
281
699
  }
282
700
 
283
701
  createEnum(name, schema) {
@@ -494,17 +912,31 @@ export function generateDataContracts(openApi, operations, options = {}) {
494
912
  const declarations = [];
495
913
  const schemas = openApi.components?.schemas ?? {};
496
914
 
497
- const entries = Object.entries(schemas).sort(([a], [b]) =>
498
- a.localeCompare(b),
915
+ const genericDefinitions = [...resolver.genericDefinitions.values()].sort((a, b) =>
916
+ a.definitionName.localeCompare(b.definitionName),
499
917
  );
500
- for (const [rawName, schemaOrRef] of entries) {
501
- const schema = dereference(
918
+ for (const metadata of genericDefinitions) {
919
+ const sourceSchema = dereference(
502
920
  openApi,
503
- schemaOrRef,
504
- "Component schema reference",
921
+ schemas[metadata.rawName],
922
+ "Generic component schema reference",
505
923
  );
924
+ const schema = metadata.synthetic
925
+ ? copySelectedProperties(sourceSchema, metadata.syntheticProperties)
926
+ : sourceSchema;
927
+ declarations.push(resolver.createGenericDeclaration(metadata, schema));
928
+ }
929
+
930
+ const entries = Object.entries(schemas).sort(([a], [b]) => a.localeCompare(b));
931
+ for (const [rawName, schemaOrRef] of entries) {
932
+ if (resolver.genericSchemas.has(rawName)) continue;
933
+ const schema = dereference(openApi, schemaOrRef, "Component schema reference");
934
+ if (resolver.isPolymorphicBase(rawName)) {
935
+ declarations.push(...resolver.createPolymorphicDeclarations(rawName, schema));
936
+ continue;
937
+ }
506
938
  declarations.push(
507
- resolver.createDeclaration(resolver.componentName(rawName), schema),
939
+ resolver.createDeclaration(resolver.componentName(rawName), schema, { rawName }),
508
940
  );
509
941
  }
510
942
 
@@ -557,9 +989,9 @@ export function generateDataContracts(openApi, operations, options = {}) {
557
989
  }
558
990
  }
559
991
 
560
- for (const [name, schema] of resolver.inlineTypes.entries()) {
561
- if (!resolver.componentNamesHasValue(name))
562
- declarations.push(resolver.createDeclaration(name, schema));
992
+ for (const [name, inline] of resolver.inlineTypes.entries()) {
993
+ if (!resolver.componentNamesHasValue(name) && !resolver.genericDefinitions.has(name))
994
+ declarations.push(resolver.createDeclaration(name, inline.schema, inline.options));
563
995
  }
564
996
 
565
997