UNPKG

openapi-typescript

Version:

Generate TypeScript types from Swagger OpenAPI specs

189 lines (160 loc) 6.86 kB
import { GlobalContext } from "../types"; import { comment, nodeType, tsArrayOf, tsIntersectionOf, tsPartial, tsReadonly, tsTupleOf, tsUnionOf } from "../utils"; interface TransformSchemaObjOptions extends GlobalContext { required: Set<string>; } function hasDefaultValue(node: any): boolean { if (node.hasOwnProperty("default")) return true; // if (node.hasOwnProperty("$ref")) return true; // TODO: resolve remote $refs? return false; } /** Take object keys and convert to TypeScript interface */ export function transformSchemaObjMap(obj: Record<string, any>, options: TransformSchemaObjOptions): string { let output = ""; for (const k of Object.keys(obj)) { const v = obj[k]; // 1. JSDoc comment (goes above property) if (v.description) output += comment(v.description); // 2. name (with “?” if optional property) const readonly = tsReadonly(options.immutableTypes); const required = options.required.has(k) || (options.defaultNonNullable && hasDefaultValue(v.schema || v)) ? "" : "?"; output += `${readonly}"${k}"${required}: `; // 3. transform output += transformSchemaObj(v.schema || v, options); // 4. close output += `;\n`; } return output.replace(/\n+$/, "\n"); // replace repeat line endings with only one } /** make sure all required fields exist **/ export function addRequiredProps(properties: Record<string, any>, required: Set<string>): string[] { const missingRequired = [...required].filter((r: string) => !(r in properties)); if (missingRequired.length == 0) { return []; } let output = ""; for (const r of missingRequired) { output += `${r}: unknown;\n`; } return [`{\n${output}}`]; } /** transform anyOf */ export function transformAnyOf(anyOf: any, options: TransformSchemaObjOptions): string { // filter out anyOf keys that only have a `required` key. #642 const schemas = anyOf.filter((s: any) => { if (Object.keys(s).length > 1) return true; if (s.required) return false; return true; }); if (schemas.length === 0) { return ""; } return tsIntersectionOf(schemas.map((s: any) => tsPartial(transformSchemaObj(s, options)))); } /** transform oneOf */ export function transformOneOf(oneOf: any, options: TransformSchemaObjOptions): string { return tsUnionOf(oneOf.map((value: any) => transformSchemaObj(value, options))); } /** Convert schema object to TypeScript */ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions): string { const readonly = tsReadonly(options.immutableTypes); let output = ""; // pass in formatter, if specified const overriddenType = options.formatter && options.formatter(node); // open nullable if (node.nullable) { output += "("; } if (overriddenType) { output += overriddenType; } else { // transform core type switch (nodeType(node)) { case "ref": { output += node.$ref; // these were transformed at load time when remote schemas were resolved; return as-is break; } case "string": case "number": case "boolean": case "unknown": { output += nodeType(node) || "any"; break; } case "enum": { const items: Array<string | number | boolean> = []; (node.enum as unknown[]).forEach((item) => { if (typeof item === "string") items.push(`'${item.replace(/'/g, "\\'")}'`); else if (typeof item === "number" || typeof item === "boolean") items.push(item); else if (item === null && !node.nullable) items.push("null"); }); output += tsUnionOf(items); break; } case "object": { const isAnyOfOrOneOfOrAllOf = "anyOf" in node || "oneOf" in node || "allOf" in node; const missingRequired = addRequiredProps(node.properties || {}, node.required || []); // if empty object, then return generic map type if ( !isAnyOfOrOneOfOrAllOf && (!node.properties || !Object.keys(node.properties).length) && !node.additionalProperties ) { const emptyObj = `{ ${readonly}[key: string]: unknown }`; output += tsIntersectionOf([emptyObj, ...missingRequired]); break; } let properties = transformSchemaObjMap(node.properties || {}, { ...options, required: new Set(node.required || []), }); // if additional properties, add an intersection with a generic map type let additionalProperties: string | undefined; if ( node.additionalProperties || (node.additionalProperties === undefined && options.additionalProperties && options.version === 3) ) { if ((node.additionalProperties ?? true) === true || Object.keys(node.additionalProperties).length === 0) { additionalProperties = `{ ${readonly}[key: string]: any }`; } else if (typeof node.additionalProperties === "object") { const oneOf: any[] | undefined = (node.additionalProperties as any).oneOf || undefined; // TypeScript does a really bad job at inference here, so we enforce a type const anyOf: any[] | undefined = (node.additionalProperties as any).anyOf || undefined; // " if (oneOf) { additionalProperties = `{ ${readonly}[key: string]: ${transformOneOf(oneOf, options)}; }`; } else if (anyOf) { additionalProperties = `{ ${readonly}[key: string]: ${transformAnyOf(anyOf, options)}; }`; } else { additionalProperties = `{ ${readonly}[key: string]: ${ transformSchemaObj(node.additionalProperties, options) || "any" }; }`; } } } output += tsIntersectionOf([ // append allOf/anyOf/oneOf first ...(node.allOf ? (node.allOf as any[]).map((node) => transformSchemaObj(node, options)) : []), ...(node.anyOf ? [transformAnyOf(node.anyOf, options)] : []), ...(node.oneOf ? [transformOneOf(node.oneOf, options)] : []), ...(properties ? [`{\n${properties}\n}`] : []), // then properties (line breaks are important!) ...missingRequired, // add required that are missing from properties ...(additionalProperties ? [additionalProperties] : []), // then additional properties ]); break; } case "array": { if (Array.isArray(node.items)) { output += `${readonly}${tsTupleOf(node.items.map((node: any) => transformSchemaObj(node, options)))}`; } else { output += `${readonly}${tsArrayOf(node.items ? transformSchemaObj(node.items as any, options) : "any")}`; } break; } } } // close nullable if (node.nullable) { output += ") | null"; } return output; }