Skip to content

Commit

Permalink
Simplify minItems / maxItems tuple generation
Browse files Browse the repository at this point in the history
  • Loading branch information
duncanbeevers committed Jan 3, 2025
1 parent 9b38b53 commit 40c0c98
Showing 1 changed file with 68 additions and 0 deletions.
68 changes: 68 additions & 0 deletions packages/openapi-typescript/src/transform/schema-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,74 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr
return toOptionsReadonly(tupleType, options);
}

type ArraySchemaObject = SchemaObject & ArraySubtype;
function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): schemaObject is ArraySchemaObject {
return schemaObject.type === "array";
}

function padTupleMembers(length: number, itemType: ts.TypeNode, prefixTypes: readonly ts.TypeNode[]) {
return Array.from({ length }).map((_, index) => {
return prefixTypes[index] ?? itemType;
});
}

function toOptionsReadonly<TMembers extends ts.ArrayTypeNode | ts.TupleTypeNode>(
members: TMembers,
options: TransformNodeOptions,
): TMembers | ts.TypeOperatorNode {
return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, members) : members;
}

/* Transform Array schema object */
function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode {
const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options));

if (Array.isArray(schemaObject.items)) {
throw new Error(`${options.path}: invalid property items. Expected Schema Object, got Array`);
}

const itemType = schemaObject.items ? transformSchemaObject(schemaObject.items, options) : UNKNOWN;

// The minimum number of tuple members to return
const min: number =
options.ctx.arrayLength && typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0
? schemaObject.minItems
: 0;
const max: number | undefined =
options.ctx.arrayLength &&
typeof schemaObject.maxItems === "number" &&
schemaObject.maxItems >= 0 &&
min <= schemaObject.maxItems
? schemaObject.maxItems
: undefined;

// "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice
const MAX_CODE_SIZE = 30;
const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2;
const shouldGeneratePermutations = (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE;

// if maxItems is set, then return a union of all permutations of possible tuple types
if (shouldGeneratePermutations && max !== undefined) {
return tsUnion(
Array.from({ length: max - min + 1 }).map((_, index) =>
toOptionsReadonly(ts.factory.createTupleTypeNode(padTupleMembers(index + min, itemType, prefixTypes)), options),
),
);
}

// if maxItems not set, then return a simple tuple type the length of `min`
const spreadType = ts.factory.createArrayTypeNode(itemType);
const tupleType =
shouldGeneratePermutations || prefixTypes.length
? ts.factory.createTupleTypeNode([
...padTupleMembers(Math.max(min, prefixTypes.length), itemType, prefixTypes),
ts.factory.createRestTypeNode(toOptionsReadonly(spreadType, options)),
])
: spreadType;

return toOptionsReadonly(tupleType, options);
}

/**
* Handle SchemaObject minus composition (anyOf/allOf/oneOf)
*/
Expand Down

0 comments on commit 40c0c98

Please sign in to comment.