UNPKG

openapi-typescript

Version:

Generate TypeScript types from Swagger OpenAPI specs

168 lines (145 loc) 5.86 kB
import { GlobalContext } from "../types"; import { comment, nodeType, transformRef, 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 } /** transform anyOf */ export function transformAnyOf(anyOf: any, options: TransformSchemaObjOptions): string { return tsIntersectionOf(anyOf.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 = ""; // open nullable if (node.nullable) { output += "("; } // pass in formatter, if specified const overriddenType = options.formatter && options.formatter(node); if (overriddenType) { output += overriddenType; } else { // transform core type switch (nodeType(node)) { case "ref": { output += transformRef(node.$ref); break; } case "string": case "number": case "boolean": { 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; // if empty object, then return generic map type if ( !isAnyOfOrOneOfOrAllOf && (!node.properties || !Object.keys(node.properties).length) && !node.additionalProperties ) { output += `{ ${readonly}[key: string]: any }`; 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!) ...(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; }