Skip to content

Commit 19be4e4

Browse files
committed
Fix nested tuples
1 parent d309753 commit 19be4e4

File tree

2 files changed

+74
-56
lines changed

2 files changed

+74
-56
lines changed

packages/openapi-typescript/src/transform/schema-object.ts

Lines changed: 47 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -310,72 +310,64 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
310310

311311
// type: array (with support for tuples)
312312
if (schemaObject.type === "array") {
313-
// default to `unknown[]`
314-
let itemType: ts.TypeNode = UNKNOWN;
315-
// tuple type
316-
if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) {
317-
const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]);
318-
itemType = ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options)));
319-
}
320-
// standard array type
321-
else if (schemaObject.items) {
322-
if (hasKey(schemaObject.items, "type") && schemaObject.items.type === "array") {
323-
itemType = ts.factory.createArrayTypeNode(transformSchemaObject(schemaObject.items, options));
324-
} else {
325-
itemType = transformSchemaObject(schemaObject.items, options);
313+
const arrayType = (() => {
314+
// tuple type
315+
if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) {
316+
const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]);
317+
return ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options)));
326318
}
327-
}
328319

329-
const min: number =
330-
typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0;
331-
const max: number | undefined =
332-
typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems
333-
? schemaObject.maxItems
334-
: undefined;
335-
const estimateCodeSize = typeof max !== "number" ? min : (max * (max + 1) - min * (min - 1)) / 2;
336-
if (
337-
options.ctx.arrayLength &&
338-
(min !== 0 || max !== undefined) &&
339-
estimateCodeSize < 30 // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice
340-
) {
341-
if (min === max) {
342-
const elements: ts.TypeNode[] = [];
343-
for (let i = 0; i < min; i++) {
344-
elements.push(itemType);
345-
}
346-
return tsUnion([ts.factory.createTupleTypeNode(elements)]);
347-
} else if ((schemaObject.maxItems as number) > 0) {
348-
// if maxItems is set, then return a union of all permutations of possible tuple types
349-
const members: ts.TypeNode[] = [];
350-
// populate 1 short of min …
351-
for (let i = 0; i <= (max ?? 0) - min; i++) {
320+
// standard array type
321+
const itemType: ts.TypeNode = schemaObject.items ? transformSchemaObject(schemaObject.items, options) : UNKNOWN;
322+
323+
const min: number =
324+
typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0;
325+
const max: number | undefined =
326+
typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems
327+
? schemaObject.maxItems
328+
: undefined;
329+
const estimateCodeSize = typeof max !== "number" ? min : (max * (max + 1) - min * (min - 1)) / 2;
330+
if (
331+
options.ctx.arrayLength &&
332+
(min !== 0 || max !== undefined) &&
333+
estimateCodeSize < 30 // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice
334+
) {
335+
if (min === max) {
352336
const elements: ts.TypeNode[] = [];
353-
for (let j = min; j < i + min; j++) {
337+
for (let i = 0; i < min; i++) {
354338
elements.push(itemType);
355339
}
356-
members.push(ts.factory.createTupleTypeNode(elements));
340+
return tsUnion([ts.factory.createTupleTypeNode(elements)]);
341+
} else if ((schemaObject.maxItems as number) > 0) {
342+
// if maxItems is set, then return a union of all permutations of possible tuple types
343+
const members: ts.TypeNode[] = [];
344+
// populate 1 short of min …
345+
for (let i = 0; i <= (max ?? 0) - min; i++) {
346+
const elements: ts.TypeNode[] = [];
347+
for (let j = min; j < i + min; j++) {
348+
elements.push(itemType);
349+
}
350+
members.push(ts.factory.createTupleTypeNode(elements));
351+
}
352+
return tsUnion(members);
357353
}
358-
return tsUnion(members);
359-
}
360-
// if maxItems not set, then return a simple tuple type the length of `min`
361-
else {
362-
const elements: ts.TypeNode[] = [];
363-
for (let i = 0; i < min; i++) {
364-
elements.push(itemType);
354+
// if maxItems not set, then return a simple tuple type the length of `min`
355+
else {
356+
const elements: ts.TypeNode[] = [];
357+
for (let i = 0; i < min; i++) {
358+
elements.push(itemType);
359+
}
360+
elements.push(ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType)));
361+
return ts.factory.createTupleTypeNode(elements);
365362
}
366-
elements.push(ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType)));
367-
return ts.factory.createTupleTypeNode(elements);
368363
}
369-
}
370364

371-
const finalType =
372-
ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType)
373-
? itemType
374-
: ts.factory.createArrayTypeNode(itemType); // wrap itemType in array type, but only if not a tuple or array already
365+
return ts.factory.createArrayTypeNode(itemType);
366+
})();
375367

376368
return options.ctx.immutable
377-
? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType)
378-
: finalType;
369+
? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, arrayType)
370+
: arrayType;
379371
}
380372

381373
// polymorphic, or 3.1 nullable

packages/openapi-typescript/test/transform/schema-object/array.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ describe("transformSchemaObject > array", () => {
1515
{
1616
given: { type: "array", items: { type: "string" } },
1717
want: "string[]",
18-
// options: DEFAULT_OPTIONS,
18+
},
19+
],
20+
[
21+
"nested",
22+
{
23+
given: { type: "array", items: { type: "array", items: { type: "string" } } },
24+
want: "string[][]",
1925
},
2026
],
2127
// Prevents: "TypeError: Cannot use 'in' operator to search for 'type' in true"
@@ -161,6 +167,26 @@ describe("transformSchemaObject > array", () => {
161167
want: `[
162168
string,
163169
string
170+
]`,
171+
options: {
172+
...DEFAULT_OPTIONS,
173+
ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true },
174+
},
175+
},
176+
],
177+
[
178+
"options > arrayLength: true > minItems: 1, maxItems: 1; minItems: 1, maxItems: 1",
179+
{
180+
given: {
181+
type: "array",
182+
items: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 1 },
183+
minItems: 1,
184+
maxItems: 1,
185+
},
186+
want: `[
187+
[
188+
string
189+
]
164190
]`,
165191
options: {
166192
...DEFAULT_OPTIONS,

0 commit comments

Comments
 (0)