Skip to content

Commit 40c0c98

Browse files
committed
Simplify minItems / maxItems tuple generation
Closes openapi-ts#2048
1 parent 9b38b53 commit 40c0c98

File tree

1 file changed

+68
-0
lines changed

1 file changed

+68
-0
lines changed

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

+68
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,74 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr
529529
return toOptionsReadonly(tupleType, options);
530530
}
531531

532+
type ArraySchemaObject = SchemaObject & ArraySubtype;
533+
function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): schemaObject is ArraySchemaObject {
534+
return schemaObject.type === "array";
535+
}
536+
537+
function padTupleMembers(length: number, itemType: ts.TypeNode, prefixTypes: readonly ts.TypeNode[]) {
538+
return Array.from({ length }).map((_, index) => {
539+
return prefixTypes[index] ?? itemType;
540+
});
541+
}
542+
543+
function toOptionsReadonly<TMembers extends ts.ArrayTypeNode | ts.TupleTypeNode>(
544+
members: TMembers,
545+
options: TransformNodeOptions,
546+
): TMembers | ts.TypeOperatorNode {
547+
return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, members) : members;
548+
}
549+
550+
/* Transform Array schema object */
551+
function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode {
552+
const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options));
553+
554+
if (Array.isArray(schemaObject.items)) {
555+
throw new Error(`${options.path}: invalid property items. Expected Schema Object, got Array`);
556+
}
557+
558+
const itemType = schemaObject.items ? transformSchemaObject(schemaObject.items, options) : UNKNOWN;
559+
560+
// The minimum number of tuple members to return
561+
const min: number =
562+
options.ctx.arrayLength && typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0
563+
? schemaObject.minItems
564+
: 0;
565+
const max: number | undefined =
566+
options.ctx.arrayLength &&
567+
typeof schemaObject.maxItems === "number" &&
568+
schemaObject.maxItems >= 0 &&
569+
min <= schemaObject.maxItems
570+
? schemaObject.maxItems
571+
: undefined;
572+
573+
// "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice
574+
const MAX_CODE_SIZE = 30;
575+
const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2;
576+
const shouldGeneratePermutations = (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE;
577+
578+
// if maxItems is set, then return a union of all permutations of possible tuple types
579+
if (shouldGeneratePermutations && max !== undefined) {
580+
return tsUnion(
581+
Array.from({ length: max - min + 1 }).map((_, index) =>
582+
toOptionsReadonly(ts.factory.createTupleTypeNode(padTupleMembers(index + min, itemType, prefixTypes)), options),
583+
),
584+
);
585+
}
586+
587+
// if maxItems not set, then return a simple tuple type the length of `min`
588+
const spreadType = ts.factory.createArrayTypeNode(itemType);
589+
const tupleType =
590+
shouldGeneratePermutations || prefixTypes.length
591+
? ts.factory.createTupleTypeNode([
592+
...padTupleMembers(Math.max(min, prefixTypes.length), itemType, prefixTypes),
593+
ts.factory.createRestTypeNode(toOptionsReadonly(spreadType, options)),
594+
])
595+
: spreadType;
596+
597+
return toOptionsReadonly(tupleType, options);
598+
}
599+
532600
/**
533601
* Handle SchemaObject minus composition (anyOf/allOf/oneOf)
534602
*/

0 commit comments

Comments
 (0)