@@ -10,6 +10,7 @@ import {
10
10
UNDEFINED,
11
11
UNKNOWN,
12
12
addJSDocComment,
13
+ astToString,
13
14
oapiRef,
14
15
tsArrayLiteralExpression,
15
16
tsEnum,
@@ -25,7 +26,7 @@ import {
25
26
tsWithRequired,
26
27
} from "../lib/ts.js";
27
28
import { createDiscriminatorProperty, createRef, getEntries } from "../lib/utils.js";
28
- import type { ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js";
29
+ import type { ArraySubtype, ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js";
29
30
30
31
/**
31
32
* Transform SchemaObject nodes (4.8.24)
@@ -273,6 +274,74 @@ export function transformSchemaObjectWithComposition(
273
274
return finalType;
274
275
}
275
276
277
+ type ArraySchemaObject = SchemaObject & ArraySubtype;
278
+ function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): schemaObject is ArraySchemaObject {
279
+ return schemaObject.type === "array";
280
+ }
281
+
282
+ function padTupleMembers(length: number, itemType: ts.TypeNode, prefixTypes: readonly ts.TypeNode[]) {
283
+ return Array.from({ length }).map((_, index) => {
284
+ return prefixTypes[index] ?? itemType;
285
+ });
286
+ }
287
+
288
+ function toOptionsReadonly<TMembers extends ts.ArrayTypeNode | ts.TupleTypeNode>(
289
+ members: TMembers,
290
+ options: TransformNodeOptions,
291
+ ): TMembers | ts.TypeOperatorNode {
292
+ return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, members) : members;
293
+ }
294
+
295
+ /* Transform Array schema object */
296
+ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode {
297
+ const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options));
298
+
299
+ if (Array.isArray(schemaObject.items)) {
300
+ throw new Error(`${options.path}: invalid property items. Expected Schema Object, got Array`);
301
+ }
302
+
303
+ const itemType = schemaObject.items ? transformSchemaObject(schemaObject.items, options) : UNKNOWN;
304
+
305
+ // The minimum number of tuple members to return
306
+ const min: number =
307
+ options.ctx.arrayLength && typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0
308
+ ? schemaObject.minItems
309
+ : 0;
310
+ const max: number | undefined =
311
+ options.ctx.arrayLength &&
312
+ typeof schemaObject.maxItems === "number" &&
313
+ schemaObject.maxItems >= 0 &&
314
+ min <= schemaObject.maxItems
315
+ ? schemaObject.maxItems
316
+ : undefined;
317
+
318
+ // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice
319
+ const MAX_CODE_SIZE = 30;
320
+ const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2;
321
+ const shouldGeneratePermutations = (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE;
322
+
323
+ // if maxItems is set, then return a union of all permutations of possible tuple types
324
+ if (shouldGeneratePermutations && max !== undefined) {
325
+ return tsUnion(
326
+ Array.from({ length: max - min + 1 }).map((_, index) =>
327
+ toOptionsReadonly(ts.factory.createTupleTypeNode(padTupleMembers(index + min, itemType, prefixTypes)), options),
328
+ ),
329
+ );
330
+ }
331
+
332
+ // if maxItems not set, then return a simple tuple type the length of `min`
333
+ const spreadType = ts.factory.createArrayTypeNode(itemType);
334
+ const tupleType =
335
+ shouldGeneratePermutations || prefixTypes.length
336
+ ? ts.factory.createTupleTypeNode([
337
+ ...padTupleMembers(Math.max(min, prefixTypes.length), itemType, prefixTypes),
338
+ ts.factory.createRestTypeNode(toOptionsReadonly(spreadType, options)),
339
+ ])
340
+ : spreadType;
341
+
342
+ return toOptionsReadonly(tupleType, options);
343
+ }
344
+
276
345
/**
277
346
* Handle SchemaObject minus composition (anyOf/allOf/oneOf)
278
347
*/
@@ -312,73 +381,8 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
312
381
}
313
382
314
383
// 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;
384
+ if (isArraySchemaObject(schemaObject)) {
385
+ return transformArraySchemaObject(schemaObject, options);
382
386
}
383
387
384
388
// polymorphic, or 3.1 nullable
0 commit comments