Skip to content

Commit 810054b

Browse files
committed
Fix prefixItems / minItems / maxItems tuple generation (openapi-ts#2053)
* Simplify minItems / maxItems tuple generation Closes openapi-ts#2048
1 parent 82e98b4 commit 810054b

File tree

6 files changed

+473
-86
lines changed

6 files changed

+473
-86
lines changed

packages/openapi-typescript/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"yargs-parser": "^21.1.1"
6969
},
7070
"devDependencies": {
71+
"@total-typescript/ts-reset": "^0.6.1",
7172
"@types/degit": "^2.8.6",
7273
"@types/js-yaml": "^4.0.9",
7374
"degit": "^2.8.4",
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Do not add any other lines of code to this file!
2+
import "@total-typescript/ts-reset";

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

+93-68
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
tsWithRequired,
2626
} from "../lib/ts.js";
2727
import { createDiscriminatorProperty, createRef, getEntries } from "../lib/utils.js";
28-
import type { ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js";
28+
import type { ArraySubtype, ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js";
2929

3030
/**
3131
* Transform SchemaObject nodes (4.8.24)
@@ -273,6 +273,96 @@ export function transformSchemaObjectWithComposition(
273273
return finalType;
274274
}
275275

276+
type ArraySchemaObject = SchemaObject & ArraySubtype;
277+
function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): schemaObject is ArraySchemaObject {
278+
return schemaObject.type === "array";
279+
}
280+
281+
/**
282+
* Pad out the end of tuple members with item item
283+
* @param prefixTypes The array before any padding occurs
284+
* @param length The length of the returned array
285+
* @param itemType The type to pad out the end of the array with
286+
*/
287+
function padTupleMembers(prefixTypes: readonly ts.TypeNode[], length: number, itemType: ts.TypeNode) {
288+
return Array.from({ length }).map((_, index) =>
289+
index < prefixTypes.length ? prefixTypes[index] : itemType
290+
);
291+
}
292+
293+
function toOptionsReadonly<TMembers extends ts.ArrayTypeNode | ts.TupleTypeNode>(
294+
members: TMembers,
295+
options: TransformNodeOptions,
296+
): TMembers | ts.TypeOperatorNode {
297+
return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, members) : members;
298+
}
299+
300+
/* Transform Array schema object */
301+
function transformArraySchemaObject(
302+
schemaObject: ArraySchemaObject,
303+
options: TransformNodeOptions,
304+
): ts.TypeNode | undefined {
305+
const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options));
306+
307+
if (Array.isArray(schemaObject.items)) {
308+
return ts.factory.createTupleTypeNode(
309+
schemaObject.items.map((tupleItem) => transformSchemaObject(tupleItem, options)),
310+
);
311+
}
312+
313+
const itemType =
314+
// @ts-expect-error TS2367
315+
schemaObject.items === false
316+
? undefined
317+
: schemaObject.items
318+
? transformSchemaObject(schemaObject.items, options)
319+
: UNKNOWN;
320+
321+
// The minimum number of tuple members in the return value
322+
const min: number =
323+
options.ctx.arrayLength && typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0
324+
? schemaObject.minItems
325+
: 0;
326+
const max: number | undefined =
327+
options.ctx.arrayLength &&
328+
typeof schemaObject.maxItems === "number" &&
329+
schemaObject.maxItems >= 0 &&
330+
min <= schemaObject.maxItems
331+
? schemaObject.maxItems
332+
: undefined;
333+
334+
// "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice
335+
const MAX_CODE_SIZE = 30;
336+
const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2;
337+
const shouldGeneratePermutations = (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE;
338+
339+
// if maxItems is set, then return a union of all permutations of possible tuple types
340+
if (shouldGeneratePermutations && max !== undefined && itemType) {
341+
return tsUnion(
342+
Array.from({ length: max - min + 1 }).map((_, index) => {
343+
return toOptionsReadonly(
344+
ts.factory.createTupleTypeNode(padTupleMembers(prefixTypes, index + min, itemType)),
345+
options,
346+
);
347+
}),
348+
);
349+
}
350+
351+
// if maxItems not set, then return a simple tuple type the length of `min`
352+
const spreadType = itemType ? ts.factory.createArrayTypeNode(itemType) : undefined;
353+
const tupleType =
354+
shouldGeneratePermutations || prefixTypes.length
355+
? ts.factory.createTupleTypeNode(
356+
[
357+
...(itemType ? padTupleMembers(prefixTypes, Math.max(min, prefixTypes.length), itemType) : prefixTypes),
358+
spreadType ? ts.factory.createRestTypeNode(toOptionsReadonly(spreadType, options)) : undefined,
359+
].filter(Boolean),
360+
)
361+
: spreadType;
362+
363+
return tupleType ? toOptionsReadonly(tupleType, options) : undefined;
364+
}
365+
276366
/**
277367
* Handle SchemaObject minus composition (anyOf/allOf/oneOf)
278368
*/
@@ -312,73 +402,8 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
312402
}
313403

314404
// type: array (with support for tuples)
315-
if (schemaObject.type === "array") {
316-
// default to `unknown[]`
317-
let itemType: ts.TypeNode = UNKNOWN;
318-
// tuple type
319-
if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) {
320-
const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]);
321-
itemType = ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options)));
322-
}
323-
// standard array type
324-
else if (schemaObject.items) {
325-
if ("type" in schemaObject.items && schemaObject.items.type === "array") {
326-
itemType = ts.factory.createArrayTypeNode(transformSchemaObject(schemaObject.items, options));
327-
} else {
328-
itemType = transformSchemaObject(schemaObject.items, options);
329-
}
330-
}
331-
332-
const min: number =
333-
typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0;
334-
const max: number | undefined =
335-
typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems
336-
? schemaObject.maxItems
337-
: undefined;
338-
const estimateCodeSize = typeof max !== "number" ? min : (max * (max + 1) - min * (min - 1)) / 2;
339-
if (
340-
options.ctx.arrayLength &&
341-
(min !== 0 || max !== undefined) &&
342-
estimateCodeSize < 30 // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice
343-
) {
344-
if (min === max) {
345-
const elements: ts.TypeNode[] = [];
346-
for (let i = 0; i < min; i++) {
347-
elements.push(itemType);
348-
}
349-
return tsUnion([ts.factory.createTupleTypeNode(elements)]);
350-
} else if ((schemaObject.maxItems as number) > 0) {
351-
// if maxItems is set, then return a union of all permutations of possible tuple types
352-
const members: ts.TypeNode[] = [];
353-
// populate 1 short of min …
354-
for (let i = 0; i <= (max ?? 0) - min; i++) {
355-
const elements: ts.TypeNode[] = [];
356-
for (let j = min; j < i + min; j++) {
357-
elements.push(itemType);
358-
}
359-
members.push(ts.factory.createTupleTypeNode(elements));
360-
}
361-
return tsUnion(members);
362-
}
363-
// if maxItems not set, then return a simple tuple type the length of `min`
364-
else {
365-
const elements: ts.TypeNode[] = [];
366-
for (let i = 0; i < min; i++) {
367-
elements.push(itemType);
368-
}
369-
elements.push(ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType)));
370-
return ts.factory.createTupleTypeNode(elements);
371-
}
372-
}
373-
374-
const finalType =
375-
ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType)
376-
? itemType
377-
: ts.factory.createArrayTypeNode(itemType); // wrap itemType in array type, but only if not a tuple or array already
378-
379-
return options.ctx.immutable
380-
? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType)
381-
: finalType;
405+
if (isArraySchemaObject(schemaObject)) {
406+
return transformArraySchemaObject(schemaObject, options);
382407
}
383408

384409
// polymorphic, or 3.1 nullable

0 commit comments

Comments
 (0)