diff --git a/.changeset/clean-phones-deliver.md b/.changeset/clean-phones-deliver.md new file mode 100644 index 000000000..6a16a7b47 --- /dev/null +++ b/.changeset/clean-phones-deliver.md @@ -0,0 +1,10 @@ +--- +"openapi-typescript": minor +--- + +Extract types generation for Array-type schemas to `transformArraySchemaObject` method. +Generate correct number of union members for `minItems` * `maxItems` unions. +Generate readonly tuple members for `minItems` & `maxItems` unions. +Generate readonly spread member for `prefixItems` tuple. +Preserve `prefixItems` type members in `minItems` & `maxItems` tuples. +Generate spread member for `prefixItems` tuple with no `minItems` / `maxItems` constraints. diff --git a/packages/openapi-typescript/bin/cli.js b/packages/openapi-typescript/bin/cli.js index d40bd6606..f7af780f0 100755 --- a/packages/openapi-typescript/bin/cli.js +++ b/packages/openapi-typescript/bin/cli.js @@ -34,6 +34,10 @@ Options --root-types-no-schema-prefix (optional) Do not add "Schema" prefix to types at the root level (should only be used with --root-types) --make-paths-enum Generate ApiPaths enum for all paths + +Experimental features + --experimental-spread-array-members + Array schemas with prefixItems spread remaining items `; const OUTPUT_FILE = "FILE"; @@ -77,6 +81,7 @@ const flags = parser(args, { "dedupeEnums", "check", "excludeDeprecated", + "experimentalArraySpreadMembers", "exportType", "help", "immutable", @@ -133,7 +138,6 @@ async function generateSchema(schema, { redocly, silent = false }) { additionalProperties: flags.additionalProperties, alphabetize: flags.alphabetize, arrayLength: flags.arrayLength, - contentNever: flags.contentNever, propertiesRequiredByDefault: flags.propertiesRequiredByDefault, defaultNonNullable: flags.defaultNonNullable, emptyObjectsUnknown: flags.emptyObjectsUnknown, @@ -142,6 +146,7 @@ async function generateSchema(schema, { redocly, silent = false }) { dedupeEnums: flags.dedupeEnums, excludeDeprecated: flags.excludeDeprecated, exportType: flags.exportType, + experimentalArraySpreadMembers: flags.experimentalArraySpreadMembers, immutable: flags.immutable, pathParamsAsTypes: flags.pathParamsAsTypes, rootTypes: flags.rootTypes, diff --git a/packages/openapi-typescript/package.json b/packages/openapi-typescript/package.json index 99a2a7e8b..8c7599fab 100644 --- a/packages/openapi-typescript/package.json +++ b/packages/openapi-typescript/package.json @@ -68,6 +68,7 @@ "yargs-parser": "^21.1.1" }, "devDependencies": { + "@total-typescript/ts-reset": "^0.6.1", "@types/degit": "^2.8.6", "@types/js-yaml": "^4.0.9", "degit": "^2.8.4", diff --git a/packages/openapi-typescript/src/index.ts b/packages/openapi-typescript/src/index.ts index 8c36fd0ad..7cda7e7bd 100644 --- a/packages/openapi-typescript/src/index.ts +++ b/packages/openapi-typescript/src/index.ts @@ -77,6 +77,7 @@ export default async function openapiTS( enumValues: options.enumValues ?? false, dedupeEnums: options.dedupeEnums ?? false, excludeDeprecated: options.excludeDeprecated ?? false, + experimentalArraySpreadMembers: options.experimentalArraySpreadMembers ?? false, exportType: options.exportType ?? false, immutable: options.immutable ?? false, rootTypes: options.rootTypes ?? false, diff --git a/packages/openapi-typescript/src/lib/utils.ts b/packages/openapi-typescript/src/lib/utils.ts index 4b764b385..9b5119d12 100644 --- a/packages/openapi-typescript/src/lib/utils.ts +++ b/packages/openapi-typescript/src/lib/utils.ts @@ -155,7 +155,7 @@ export function resolveRef( return node; } -function createDiscriminatorEnum(values: string[], prevSchema?: SchemaObject): SchemaObject { +function createDiscriminatorEnum(values: string[], prevSchema?: SchemaObject | ReferenceObject): SchemaObject { return { type: "string", enum: values, @@ -167,7 +167,7 @@ function createDiscriminatorEnum(values: string[], prevSchema?: SchemaObject): S /** Adds or replaces the discriminator enum with the passed `values` in a schema defined by `ref` */ function patchDiscriminatorEnum( - schema: SchemaObject, + schema: OpenAPI3, ref: string, values: string[], discriminator: DiscriminatorObject, @@ -206,7 +206,7 @@ function patchDiscriminatorEnum( // add/replace the discriminator enum property resolvedSchema.properties[discriminator.propertyName] = createDiscriminatorEnum( values, - resolvedSchema.properties[discriminator.propertyName] as SchemaObject, + resolvedSchema.properties[discriminator.propertyName], ); return true; @@ -250,7 +250,7 @@ export function scanDiscriminators(schema: OpenAPI3, options: OpenAPITSOptions) return; } - const oneOf: (SchemaObject | ReferenceObject)[] = obj.oneOf; + const oneOf = obj.oneOf as readonly (SchemaObject | ReferenceObject)[]; const mapping: InternalDiscriminatorMapping = {}; // the mapping can be inferred from the oneOf refs next to the discriminator object @@ -301,9 +301,7 @@ export function scanDiscriminators(schema: OpenAPI3, options: OpenAPITSOptions) // biome-ignore lint/style/noNonNullAssertion: we just checked for this const mappedValues = defined ?? [inferred!]; - if ( - patchDiscriminatorEnum(schema as unknown as SchemaObject, mappedRef, mappedValues, discriminator, ref, options) - ) { + if (patchDiscriminatorEnum(schema, mappedRef, mappedValues, discriminator, ref, options)) { refsHandled.push(mappedRef); } } @@ -335,16 +333,7 @@ export function scanDiscriminators(schema: OpenAPI3, options: OpenAPITSOptions) } if (mappedValues.length > 0) { - if ( - patchDiscriminatorEnum( - schema as unknown as SchemaObject, - ref, - mappedValues, - discriminator, - item.$ref, - options, - ) - ) { + if (patchDiscriminatorEnum(schema, ref, mappedValues, discriminator, item.$ref, options)) { refsHandled.push(ref); } } diff --git a/packages/openapi-typescript/src/reset.ts b/packages/openapi-typescript/src/reset.ts new file mode 100644 index 000000000..e4d600ccb --- /dev/null +++ b/packages/openapi-typescript/src/reset.ts @@ -0,0 +1,2 @@ +// Do not add any other lines of code to this file! +import "@total-typescript/ts-reset"; diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index a41ce0e69..19bce3170 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -25,7 +25,7 @@ import { tsWithRequired, } from "../lib/ts.js"; import { createDiscriminatorProperty, createRef, getEntries } from "../lib/utils.js"; -import type { ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js"; +import type { ArraySubtype, ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js"; /** * Transform SchemaObject nodes (4.8.24) @@ -273,6 +273,97 @@ export function transformSchemaObjectWithComposition( return finalType; } +type ArraySchemaObject = SchemaObject & ArraySubtype; +function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): schemaObject is ArraySchemaObject { + return schemaObject.type === "array"; +} + +/** + * Return an array of tuple members of the given length, either by trimming + * the prefixItems, or by padding out the end of prefixItems with itemType + * @param prefixTypes The array before any padding occurs + * @param length The length of the returned array + * @param itemType The type to pad out the end of the array with + */ +function padTupleMembers(prefixTypes: readonly ts.TypeNode[], length: number, itemType: ts.TypeNode) { + return Array.from({ length }).map((_, index) => (index < prefixTypes.length ? prefixTypes[index] : itemType)); +} + +function toOptionsReadonly( + 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 | undefined { + const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options)); + + if (Array.isArray(schemaObject.items)) { + return ts.factory.createTupleTypeNode( + schemaObject.items.map((tupleItem) => transformSchemaObject(tupleItem, options)), + ); + } + + const itemType = + // @ts-expect-error TS2367 + schemaObject.items === false + ? undefined + : schemaObject.items + ? transformSchemaObject(schemaObject.items, options) + : UNKNOWN; + + // The minimum number of tuple members in the return value + 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 && itemType) { + return tsUnion( + Array.from({ length: max - min + 1 }).map((_, index) => { + return toOptionsReadonly( + ts.factory.createTupleTypeNode(padTupleMembers(prefixTypes, index + min, itemType)), + options, + ); + }), + ); + } + + // if maxItems not set, then return a simple tuple type the length of `min` + const spreadType = itemType ? ts.factory.createArrayTypeNode(itemType) : undefined; + const tupleType = + shouldGeneratePermutations || prefixTypes.length + ? ts.factory.createTupleTypeNode( + [ + ...(itemType ? padTupleMembers(prefixTypes, Math.max(min, prefixTypes.length), itemType) : prefixTypes), + spreadType && (prefixTypes.length ? options.ctx.experimentalArraySpreadMembers : true) + ? ts.factory.createRestTypeNode(toOptionsReadonly(spreadType, options)) + : undefined, + ].filter(Boolean), + ) + : spreadType; + + return tupleType ? toOptionsReadonly(tupleType, options) : undefined; +} + /** * Handle SchemaObject minus composition (anyOf/allOf/oneOf) */ @@ -312,73 +403,8 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor } // type: array (with support for tuples) - if (schemaObject.type === "array") { - // default to `unknown[]` - let itemType: ts.TypeNode = UNKNOWN; - // tuple type - if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) { - const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]); - itemType = ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options))); - } - // standard array type - else if (schemaObject.items) { - if ("type" in schemaObject.items && schemaObject.items.type === "array") { - itemType = ts.factory.createArrayTypeNode(transformSchemaObject(schemaObject.items, options)); - } else { - itemType = transformSchemaObject(schemaObject.items, options); - } - } - - const min: number = - typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0; - const max: number | undefined = - typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems - ? schemaObject.maxItems - : undefined; - const estimateCodeSize = typeof max !== "number" ? min : (max * (max + 1) - min * (min - 1)) / 2; - if ( - options.ctx.arrayLength && - (min !== 0 || max !== undefined) && - estimateCodeSize < 30 // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice - ) { - if (min === max) { - const elements: ts.TypeNode[] = []; - for (let i = 0; i < min; i++) { - elements.push(itemType); - } - return tsUnion([ts.factory.createTupleTypeNode(elements)]); - } else if ((schemaObject.maxItems as number) > 0) { - // if maxItems is set, then return a union of all permutations of possible tuple types - const members: ts.TypeNode[] = []; - // populate 1 short of min … - for (let i = 0; i <= (max ?? 0) - min; i++) { - const elements: ts.TypeNode[] = []; - for (let j = min; j < i + min; j++) { - elements.push(itemType); - } - members.push(ts.factory.createTupleTypeNode(elements)); - } - return tsUnion(members); - } - // if maxItems not set, then return a simple tuple type the length of `min` - else { - const elements: ts.TypeNode[] = []; - for (let i = 0; i < min; i++) { - elements.push(itemType); - } - elements.push(ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType))); - return ts.factory.createTupleTypeNode(elements); - } - } - - const finalType = - ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) - ? itemType - : ts.factory.createArrayTypeNode(itemType); // wrap itemType in array type, but only if not a tuple or array already - - return options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType) - : finalType; + if (isArraySchemaObject(schemaObject)) { + return transformArraySchemaObject(schemaObject, options); } // polymorphic, or 3.1 nullable diff --git a/packages/openapi-typescript/src/types.ts b/packages/openapi-typescript/src/types.ts index 75d8f8c07..df8b87930 100644 --- a/packages/openapi-typescript/src/types.ts +++ b/packages/openapi-typescript/src/types.ts @@ -647,6 +647,8 @@ export interface OpenAPITSOptions { version?: number; /** (optional) Export type instead of interface */ exportType?: boolean; + /** (optional) Experimental: Array schemas with prefixItems spread members */ + experimentalArraySpreadMembers?: boolean; /** Export true TypeScript enums instead of unions */ enum?: boolean; /** Export union values as arrays */ @@ -690,6 +692,7 @@ export interface GlobalContext { enumValues: boolean; dedupeEnums: boolean; excludeDeprecated: boolean; + experimentalArraySpreadMembers: boolean; exportType: boolean; immutable: boolean; injectFooter: ts.Node[]; diff --git a/packages/openapi-typescript/test/test-helpers.ts b/packages/openapi-typescript/test/test-helpers.ts index 72e44fccc..0abfcb11d 100644 --- a/packages/openapi-typescript/test/test-helpers.ts +++ b/packages/openapi-typescript/test/test-helpers.ts @@ -15,6 +15,7 @@ export const DEFAULT_CTX: GlobalContext = { emptyObjectsUnknown: false, enum: false, enumValues: false, + experimentalArraySpreadMembers: false, dedupeEnums: false, excludeDeprecated: false, exportType: false, diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index 59a2fddb7..e65068b95 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -15,11 +15,10 @@ describe("transformSchemaObject > array", () => { { given: { type: "array", items: { type: "string" } }, want: "string[]", - // options: DEFAULT_OPTIONS, }, ], [ - "tuple > tuple items", + "tuple > tuple items (deprecated)", { given: { type: "array", @@ -31,7 +30,21 @@ describe("transformSchemaObject > array", () => { string, number ]`, - // options: DEFAULT_OPTIONS, + }, + ], + [ + "tuple > prefixItems, items false", + { + given: { + type: "array", + items: false, + prefixItems: [{ type: "number" }, { type: "number" }, { type: "number" }], + }, + want: `[ + number, + number, + number +]`, }, ], [ @@ -47,7 +60,23 @@ describe("transformSchemaObject > array", () => { number, number ]`, - // options: DEFAULT_OPTIONS, + }, + ], + [ + "options: experimentalArraySpreadMembers: true, tuple > prefixItems", + { + given: { + type: "array", + items: { type: "number" }, + prefixItems: [{ type: "number" }, { type: "number" }, { type: "number" }], + }, + options: { ...DEFAULT_OPTIONS, ctx: { ...DEFAULT_CTX, experimentalArraySpreadMembers: true } }, + want: `[ + number, + number, + number, + ...number[] +]`, }, ], [ @@ -58,7 +87,6 @@ describe("transformSchemaObject > array", () => { items: { $ref: "#/components/schemas/ArrayItem" }, }, want: `components["schemas"]["ArrayItem"][]`, - // options: DEFAULT_OPTIONS, }, ], [ @@ -66,7 +94,6 @@ describe("transformSchemaObject > array", () => { { given: { type: ["array", "null"], items: { type: "string" } }, want: "string[] | null", - // options: DEFAULT_OPTIONS, }, ], [ @@ -74,7 +101,6 @@ describe("transformSchemaObject > array", () => { { given: { type: "array", items: { type: "string" }, nullable: true }, want: "string[] | null", - // options: DEFAULT_OPTIONS, }, ], [ @@ -82,7 +108,6 @@ describe("transformSchemaObject > array", () => { { given: { type: "array", items: { type: ["string", "null"] } }, want: "(string | null)[]", - // options: DEFAULT_OPTIONS, }, ], [ @@ -90,7 +115,38 @@ describe("transformSchemaObject > array", () => { { given: { type: "array", items: { type: "string", nullable: true } }, want: "(string | null)[]", - // options: DEFAULT_OPTIONS, + }, + ], + [ + "array > heterogeneous items", + { + given: { + type: "array", + items: { anyOf: [{ type: "number" }, { type: "string" }] }, + }, + want: "(number | string)[]", + }, + ], + [ + "options > arrayLength: false > minItems: 0", + { + given: { type: "array", items: { type: "string" }, minItems: 0 }, + want: "string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: false }, + }, + }, + ], + [ + "options > arrayLength: false > minItems: 1", + { + given: { type: "array", items: { type: "string" }, minItems: 1 }, + want: "string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: false }, + }, }, ], [ @@ -104,6 +160,28 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true > minItems: 0", + { + given: { type: "array", items: { type: "string" }, minItems: 0 }, + want: "string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true, immutable: true > minItems: 0", + { + given: { type: "array", items: { type: "string" }, minItems: 0 }, + want: "readonly string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > minItems: 1", { @@ -118,6 +196,50 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true, immutable: true > minItems: 1", + { + given: { type: "array", items: { type: "string" }, minItems: 1 }, + want: `readonly [ + string, + ...readonly string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], + [ + "options > arrayLength: true > minItems: 2", + { + given: { type: "array", items: { type: "string" }, minItems: 2 }, + want: `[ + string, + string, + ...string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true, immutable: true > minItems: 2", + { + given: { type: "array", items: { type: "string" }, minItems: 2 }, + want: `readonly [ + string, + string, + ...readonly string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > maxItems: 2", { @@ -135,6 +257,55 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true, immutable: true > maxItems: 2", + { + given: { type: "array", items: { type: "string" }, maxItems: 2 }, + want: `readonly [ +] | readonly [ + string +] | readonly [ + string, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], + [ + "options > arrayLength: true > minItems: 1, maxItems: 2", + { + given: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 2 }, + want: `[ + string +] | [ + string, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true, immutable: true > minItems: 1, maxItems: 2", + { + given: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 2 }, + want: `readonly [ + string +] | readonly [ + string, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > maxItems: 20", { @@ -146,6 +317,17 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true, immutable: true > maxItems: 20", + { + given: { type: "array", items: { type: "string" }, maxItems: 20 }, + want: "readonly string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > minItems: 2, maxItems: 2", { @@ -160,6 +342,254 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true > prefixItems, minItems: 2, maxItems: 2", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + ], + minItems: 2, + maxItems: 2, + }, + want: `[ + "calcium", + "magnesium" +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > no items, prefixItems, minItems: 2, maxItems: 3", + { + given: { + type: "array", + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + ], + minItems: 2, + maxItems: 3, + }, + want: `[ + "calcium", + "magnesium" +] | [ + "calcium", + "magnesium", + unknown +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > no items, prefixItems, minItems: 3", + { + given: { + type: "array", + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + ], + minItems: 3, + }, + want: `[ + "calcium", + "magnesium", + unknown +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true, experimentalArraySpreadMembers: true > no items, prefixItems, minItems: 3", + { + given: { + type: "array", + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + ], + minItems: 3, + }, + want: `[ + "calcium", + "magnesium", + unknown, + ...unknown[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, experimentalArraySpreadMembers: true }, + }, + }, + ], + [ + "options > arrayLength: true > no items, prefixItems, minItems: 2, maxItems: 5", + { + given: { + type: "array", + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + { type: "string", enum: ["tungsten"] }, + ], + minItems: 2, + maxItems: 5, + }, + want: `[ + "calcium", + "magnesium" +] | [ + "calcium", + "magnesium", + "tungsten" +] | [ + "calcium", + "magnesium", + "tungsten", + unknown +] | [ + "calcium", + "magnesium", + "tungsten", + unknown, + unknown +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true, immutable: true > prefixItems, minItems: 3", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [{ type: "string" }, { type: "number" }], + minItems: 3, + }, + want: `readonly [ + string, + number, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], + [ + "options > arrayLength: true, experimentalArraySpreadMembers: true, immutable: true > prefixItems, minItems: 3", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [{ type: "string" }, { type: "number" }], + minItems: 3, + }, + want: `readonly [ + string, + number, + string, + ...readonly string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true, experimentalArraySpreadMembers: true }, + }, + }, + ], + [ + "options > arrayLength: true, immutable: true > prefixItems, minItems: 3, maxItems: 3", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [{ type: "string" }, { type: "number" }], + minItems: 3, + maxItems: 3, + }, + want: `readonly [ + string, + number, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], + [ + "options > arrayLength: false, immutable: true > prefixItems, minItems: 3, maxItems: 5", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [{ type: "string" }, { type: "number" }], + minItems: 3, + maxItems: 5, + }, + want: `readonly [ + string, + number +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: false, immutable: true }, + }, + }, + ], + [ + "options > arrayLength: false, experimentalArraySpreadMembers: true, immutable: true > prefixItems, minItems: 3, maxItems: 5", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [{ type: "string" }, { type: "number" }], + minItems: 3, + maxItems: 5, + }, + want: `readonly [ + string, + number, + ...readonly string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: false, experimentalArraySpreadMembers: true, immutable: true }, + }, + }, + ], + [ + "options > arrayLength: false > prefixItems, items: false", + { + given: { + type: "array", + items: false, + prefixItems: [{ type: "string", enum: ["professor"] }], + }, + want: `[ + "professor" +]`, + }, + ], [ "options > immutable: true", { @@ -193,6 +623,26 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > experimentalArraySpreadMembers: true, immutable: true (tuple)", + { + given: { + type: "array", + items: { type: "number" }, + prefixItems: [{ type: "number" }, { type: "number" }, { type: "number" }], + }, + want: `readonly [ + number, + number, + number, + ...readonly number[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, experimentalArraySpreadMembers: true, immutable: true }, + }, + }, + ], ]; for (const [testName, { given, want, options = DEFAULT_OPTIONS, ci }] of tests) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45bc4f9b5..c1d039f5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -300,6 +300,9 @@ importers: specifier: ^21.1.1 version: 21.1.1 devDependencies: + '@total-typescript/ts-reset': + specifier: ^0.6.1 + version: 0.6.1 '@types/degit': specifier: ^2.8.6 version: 2.8.6 @@ -1743,6 +1746,9 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + '@total-typescript/ts-reset@0.6.1': + resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==} + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -6490,6 +6496,8 @@ snapshots: '@tootallnate/once@2.0.0': optional: true + '@total-typescript/ts-reset@0.6.1': {} + '@trysound/sax@0.2.0': {} '@tsconfig/node20@20.1.4': {}