diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 1f078d533..883be8d13 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -346,18 +346,16 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor if (schemaObject.type === "array") { // default to `unknown[]` let itemType: ts.TypeNode = UNKNOWN; + let isTupleType = false; // tuple type if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) { const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]); itemType = ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options))); + isTupleType = true; } // standard array type else if (schemaObject.items) { - if (hasKey(schemaObject.items, "type") && schemaObject.items.type === "array") { - itemType = ts.factory.createArrayTypeNode(transformSchemaObject(schemaObject.items, options)); - } else { - itemType = transformSchemaObject(schemaObject.items, options); - } + itemType = transformSchemaObject(schemaObject.items, options); } const min: number = @@ -381,10 +379,9 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor } else if ((schemaObject.maxItems as number) > 0) { // if maxItems is set, then return a union of all permutations of possible tuple types const members: ts.TypeNode[] = []; - // populate 1 short of min … - for (let i = 0; i <= (max ?? 0) - min; i++) { + for (let i = min; i <= (max ?? 0); i++) { const elements: ts.TypeNode[] = []; - for (let j = min; j < i + min; j++) { + for (let j = 0; j < i; j++) { elements.push(itemType); } members.push(ts.factory.createTupleTypeNode(elements)); @@ -402,10 +399,7 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor } } - const finalType = - ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) - ? itemType - : ts.factory.createArrayTypeNode(itemType); // wrap itemType in array type, but only if not a tuple or array already + const finalType = isTupleType ? itemType : ts.factory.createArrayTypeNode(itemType); return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType) diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index fe3e53358..de53c4143 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -168,6 +168,308 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true > minItems: 1, maxItems: 3", + { + given: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 3 }, + want: `[ + string +] | [ + string, + string +] | [ + string, + string, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > minItems: 0, maxItems: 2 (starts from empty)", + { + given: { type: "array", items: { type: "string" }, minItems: 0, maxItems: 2 }, + want: `[ +] | [ + string +] | [ + string, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > minItems: 3, maxItems: 5 (larger range)", + { + given: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 5 }, + want: `[ + number, + number, + number +] | [ + number, + number, + number, + number +] | [ + number, + number, + number, + number, + number +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > minItems: 2, maxItems: 3 with object items", + { + given: { + type: "array", + items: { type: "object", properties: { id: { type: "string" } } }, + minItems: 2, + maxItems: 3, + }, + want: `[ + { + id?: string; + }, + { + id?: string; + } +] | [ + { + id?: string; + }, + { + id?: string; + }, + { + id?: string; + } +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > nested array with minItems equals maxItems", + { + given: { + type: "array", + items: { type: "array", items: { type: "string" } }, + minItems: 2, + maxItems: 2, + }, + want: `[ + string[], + string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > nested array with inner minItems equals maxItems", + { + given: { + type: "array", + items: { + type: "array", + items: { type: "string" }, + minItems: 3, + maxItems: 3, + }, + }, + want: `[ + string, + string, + string +][]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > triple nested array", + { + given: { + type: "array", + items: { + type: "array", + items: { type: "array", items: { type: "string" } }, + }, + }, + want: "string[][][]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > nested array with both inner and outer constraints", + { + given: { + type: "array", + items: { + type: "array", + items: { type: "boolean" }, + minItems: 2, + maxItems: 2, + }, + minItems: 3, + maxItems: 3, + }, + want: `[ + [ + boolean, + boolean + ], + [ + boolean, + boolean + ], + [ + boolean, + boolean + ] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > nested array without constraints", + { + given: { + type: "array", + items: { type: "array", items: { type: "number" } }, + }, + want: "number[][]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > nested tuple (prefixItems) in array", + { + given: { + type: "array", + items: { + type: "array", + prefixItems: [{ type: "string" }, { type: "number" }], + }, + minItems: 2, + maxItems: 2, + }, + want: `[ + [ + string, + number + ], + [ + string, + number + ] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > nested array with minItems: 0, maxItems: 2", + { + given: { + type: "array", + items: { type: "array", items: { type: "string" } }, + minItems: 0, + maxItems: 2, + }, + want: `[ +] | [ + string[] +] | [ + string[], + string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > deeply nested with constraints at multiple levels", + { + given: { + type: "array", + items: { + type: "array", + items: { + type: "array", + items: { type: "number" }, + minItems: 2, + maxItems: 2, + }, + }, + minItems: 1, + maxItems: 1, + }, + want: `[ + [ + number, + number + ][] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > array of tuples without outer constraints", + { + given: { + type: "array", + items: { + type: "array", + prefixItems: [{ type: "string" }, { type: "boolean" }], + }, + }, + want: `[ + string, + boolean +][]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], [ "options > immutable: true", {