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