diff --git a/.changeset/eager-moles-wash.md b/.changeset/eager-moles-wash.md new file mode 100644 index 000000000..7ad24de02 --- /dev/null +++ b/.changeset/eager-moles-wash.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": patch +--- + +Fix redundant nested unions from a type array with a sibling `anyOf`/`allOf`, including when a `oneOf` sits alongside them. diff --git a/packages/openapi-typescript/examples/github-api-next.ts b/packages/openapi-typescript/examples/github-api-next.ts index 2d47a1d10..ac2efcf5f 100644 --- a/packages/openapi-typescript/examples/github-api-next.ts +++ b/packages/openapi-typescript/examples/github-api-next.ts @@ -35615,7 +35615,7 @@ export interface components { * @description User-defined metadata to store domain-specific information limited to 8 keys with scalar values. */ metadata: { - [key: string]: (null | (string | number | boolean) | (number | string | boolean) | (boolean | string | number)) | string | number | boolean; + [key: string]: (null | string | number | boolean) | string | number | boolean; }; dependency: { /** diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index caab5e10f..e5108fc3d 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -467,6 +467,22 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor if (Array.isArray(schemaObject.type) && !Array.isArray(schemaObject)) { // skip any primitive types that appear in oneOf as well const uniqueTypes: ts.TypeNode[] = []; + // The caller transforms the sibling composition keywords itself, so drop them here + // or each type member re-applies them. 'type' alone fully describes a primitive + // member; object/array members keep anyOf/allOf, because there the composition is + // what carries the shape and stripping it leaves Record / unknown[]. + const transformTypeMember = (t: string) => { + const stackable = t === "object" || t === "array"; + return transformSchemaObject( + { + ...schemaObject, + type: t, + oneOf: undefined, + ...(stackable ? {} : { anyOf: undefined, allOf: undefined }), + } as SchemaObject, + options, + ); + }; if (Array.isArray(schemaObject.oneOf)) { for (const t of schemaObject.type) { if ( @@ -475,22 +491,11 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor ) { continue; } - uniqueTypes.push( - t === "null" || t === null - ? NULL - : transformSchemaObject( - { ...schemaObject, type: t, oneOf: undefined } as SchemaObject, // don’t stack oneOf transforms - options, - ), - ); + uniqueTypes.push(t === "null" || t === null ? NULL : transformTypeMember(t)); } } else { for (const t of schemaObject.type) { - if (t === "null" || t === null) { - uniqueTypes.push(NULL); - } else { - uniqueTypes.push(transformSchemaObject({ ...schemaObject, type: t } as SchemaObject, options)); - } + uniqueTypes.push(t === "null" || t === null ? NULL : transformTypeMember(t)); } } return tsUnion(uniqueTypes); diff --git a/packages/openapi-typescript/test/transform/schema-object/composition.test.ts b/packages/openapi-typescript/test/transform/schema-object/composition.test.ts index 72e8cebb8..7d65a3de0 100644 --- a/packages/openapi-typescript/test/transform/schema-object/composition.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/composition.test.ts @@ -53,6 +53,84 @@ describe("composition", () => { // options: DEFAULT_OPTIONS, }, ], + [ + "polymorphic > anyOf + nullable", + { + given: { + type: ["string", "null"], + anyOf: [{ type: "string", format: "ipv4" }, { type: "string", format: "ipv6" }, { type: "null" }], + }, + want: "(string | null) | string | null", + // options: DEFAULT_OPTIONS, + }, + ], + [ + "polymorphic > allOf + nullable", + { + given: { + type: ["string", "null"], + allOf: [{ type: "string" }], + }, + want: "(string | null) & string", + // options: DEFAULT_OPTIONS, + }, + ], + [ + "polymorphic > allOf + nullable, multiple members", + { + given: { + type: ["string", "null"], + allOf: [{ type: "string" }, { type: "string", format: "uuid" }], + }, + want: "(string | null) & (string)", + // options: DEFAULT_OPTIONS, + }, + ], + [ + "polymorphic > anyOf + nullable array", + { + given: { + type: ["array", "null"], + anyOf: [{ type: "array", items: { type: "string" } }], + }, + want: "((unknown[] | string[]) | null) | string[]", + // options: DEFAULT_OPTIONS, + }, + ], + [ + "polymorphic > oneOf + allOf + nullable", + { + given: { + type: ["string", "null"], + oneOf: [{ type: "number" }], + allOf: [{ type: "string" }], + }, + want: "((string | null) & string) | number", + // options: DEFAULT_OPTIONS, + }, + ], + [ + "polymorphic > anyOf + nullable object", + { + given: { + type: ["null", "object"], + anyOf: [ + { type: "object", properties: { a: { type: "string" } } }, + { type: "object", properties: { b: { type: "string" } } }, + ], + }, + want: `(null | ({ + a?: string; +} | { + b?: string; +})) | { + a?: string; +} | { + b?: string; +}`, + // options: DEFAULT_OPTIONS, + }, + ], [ "oneOf > primitives", {