openapi-typescript
Version:
Generate TypeScript types from Swagger OpenAPI specs
168 lines (145 loc) • 5.86 kB
text/typescript
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;
}