@@ -25,7 +25,7 @@ import {
2525 tsWithRequired ,
2626} from "../lib/ts.js" ;
2727import { 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,95 @@ 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+ * 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 ) ? ts . factory . createRestTypeNode ( toOptionsReadonly ( spreadType , options ) ) : undefined ,
358+ ] . filter ( Boolean ) ,
359+ )
360+ : spreadType ;
361+
362+ return tupleType ? toOptionsReadonly ( tupleType , options ) : undefined ;
363+ }
364+
276365/**
277366 * Handle SchemaObject minus composition (anyOf/allOf/oneOf)
278367 */
@@ -312,73 +401,8 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
312401 }
313402
314403 // 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 ;
404+ if ( isArraySchemaObject ( schemaObject ) ) {
405+ return transformArraySchemaObject ( schemaObject , options ) ;
382406 }
383407
384408 // polymorphic, or 3.1 nullable
0 commit comments