TypeScript 接口和类型生成

接口和类型的定义是一件耗时费力的工作,通过解析 OpenAPI/Swagger 文档,自动化生成接口是一种很好的解决方案,这也便于接口更新时做 git diff,清晰的显示接口更新的内容。

1
2
3
4
5
6
7
8
9
10
11
export interface AppResetPasswordReq {
email: string
password: string
code: string
}

export function resetPassword(params: AppResetPasswordReq): Promise<Result<boolean>> {
return axios.post('/app/reset/password', params)
}

// more...

在 OpenAPI 中,paths 定义了请求路径、请求方法、注释、函数名、实体模型引用等信息, components.schemas 则定义了实体模型,包括请求字段是否必填,以及实体间的嵌套关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
{
"paths": {
"/app/reset/password": {
"post": {
"summary": "修改密码验证码",
"operationId": "resetPassword",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AppResetPasswordReq"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultBoolean"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"AppResetPasswordReq": {
"required": [
"code",
"email",
"password"
],
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "邮箱"
},
"password": {
"type": "string",
"description": "密码"
},
"code": {
"type": "string",
"description": "邮箱验证码"
}
},
"description": "忘记密码请求对象"
},
"ResultBoolean": {
"type": "object",
"properties": {
"requestId": {
"type": "string",
"description": "请求ID"
},
"success": {
"type": "boolean",
"description": "请求结果",
"example": true
},
"code": {
"type": "integer",
"description": "请求编码",
"format": "int32",
"example": 200
},
"message": {
"type": "string",
"description": "请求消息"
},
"data": {
"type": "boolean",
"description": "请求返回体"
}
}
}
}
}
}

解析时,需要注意以下几点:

1
2
3
* Java 中的一些基础类型,需要转为 JS 的基础类型,比如 integer 转为 number
* 嵌套的实体模型中,最外层为最基础实体模型 Result<T>、ListResult<T>、PageResult<T>、TreeNodeResult<T>,应提取公用生成泛型,按泛型注解
* object 类型需递归子属性类型

最终的生成程序以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import fs from 'fs';
import axios from 'axios';
import type { OpenApi, OpenApiReference, OpenApiSchema, OpenApiOperation, OpenApiResponse, OpenApiMediaType, OpenApiRequestBody, OpenApiResponses } from 'openapi-v3';

const genericityList: string[] = ['ListResult', 'Result', 'PageResult', 'PageData', 'TreeNodeResult'];
const typeMapping: Record<string, string> = {
'integer': 'number',
'Integer': 'number',
};

let tsContent = `import axios from 'axios';
import type { Result, ListResult, PageResult, TreeNodeResult } from '@/core/types/global';\n\n`;

/**
* 生成类型
*/
function generateType(openapi: OpenApi): string {
let typeContent = '';
if (!openapi.components?.schemas) {
return typeContent;
}
Object.entries(openapi.components.schemas).forEach(([typeName, schema]) => {
if (shouldSkipType(typeName)) {
return;
}

schema = schema as OpenApiSchema;
const properties = schema.properties || {};
const requiredFields = schema.required || [];

const paramsTypeDefinition = Object.entries(properties)
.map(([name, prop]) => {
const required = requiredFields.includes(name) ? '' : '?';
return `${name}${required}: ${getPropertyType(prop)};`;
})
.join('\n ');

typeContent += `export interface ${typeName} {\n ${paramsTypeDefinition}\n}\n\n`;
});
return typeContent;
}

/**
* 生成接口
*/
function generateApi(openapi: OpenApi): string {
let apiContent = '';
Object.entries(openapi.paths).forEach(([path, methods]) => {
Object.entries(methods as Record<string, OpenApiOperation>).forEach(([method, operation]) => {
const operationId = operation.operationId;
const summary = operation.summary || operation.description;
const requestBody = operation.requestBody as OpenApiRequestBody;
const responses = operation.responses as OpenApiResponses;

// 获取请求体的类型
let requestType = '';
if (requestBody?.content) {
for (const contentType in requestBody.content) {
if (requestBody.content[contentType].schema) {
const requestBodySchema = requestBody.content[contentType].schema as OpenApiReference;
const ref = requestBodySchema.$ref;
if (ref) {
const parts = ref.split('/');
requestType = parts[parts.length - 1];
requestType = handleSpecialTypes(openapi, requestType);
}
break;
}
}
}

// 获取响应体的类型
let responseType = '';
const response200 = responses['200'] as OpenApiResponse;
for (const contentType in response200.content) {
const mediaType = response200.content[contentType] as OpenApiMediaType;
if (mediaType.schema) {
const responseSchema = mediaType.schema as OpenApiReference;
const ref = responseSchema.$ref;
if (ref) {
const parts = ref.split('/');
responseType = parts[parts.length - 1];
responseType = handleSpecialTypes(openapi, responseType);
}
break;
}
}

// 构建函数名称
const functionName = operationId && operationId.replace(/[^\w]/g, '');

// 构建函数注释
const functionComment = `/**\n * ${summary}\n */`;

// 构建请求函数
const paramsType = requestType ? `params: ${requestType}` : 'params?: any';
const responseTypeDefinition = responseType ? `Promise<${responseType}>` : 'Promise<any>';
const axiosConfig = method.toLowerCase() === 'get' ? '{ params }' : 'params';
const functionDefinition = `${functionComment}\nexport function ${functionName}(\n ${paramsType}\n): ${responseTypeDefinition} {\n return axios.${method.toLowerCase()}('${path}', ${axiosConfig});\n}\n\n`;

apiContent += functionDefinition;
});
});
return apiContent;
}

/**
* 获取属性类型
*/
function getPropertyType(property: OpenApiSchema | OpenApiReference): string {
// 类型为数组,则递归生成数组项类型
if ('type' in property && property.type === 'array' && property.items) {
return `${getPropertyType(property.items)}[]`;
}
// 类型为对象,则递归生成各子属性类型
if ('type' in property && property.type === 'object' && property.properties) {
return `{ ${Object.entries(property.properties).map(([name, prop]) => `${name}: ${getPropertyType(prop)}`).join(', ')} }`;
}
// Java 基础类型则转为 JS 基础类型
if ('type' in property && property.type && typeMapping[property.type]) {
return typeMapping[property.type];
}
// 属性指向其他 schema,则取所指向的类型
if ((property as OpenApiReference).$ref) {
const ref = (property as OpenApiReference).$ref || '';
const parts = ref.split('/');
const refType = parts[parts.length - 1];
return refType;
}
return 'type' in property && property.type || 'any';
}

/**
* 是否跳过公共类型
* 公共类型被提取为泛型,无需生成
*/
function shouldSkipType(typeName: string): boolean {
return genericityList.some(item => typeName.startsWith(item));
}

/**
* 处理特殊类型
*/
function handleSpecialTypes(openapi: OpenApi, typeName: string): string {
// 如果以泛型做前缀,则解析为泛型
genericityList.forEach(item => {
if (typeName.startsWith(item)) {
typeName = `${item}<${getInnerType(openapi, typeName, item)}>`;
}
});
return typeName;
}

/**
* 获取泛型参数类型
*/
function getInnerType(openapi: OpenApi, typeName: string, prefix: string): string {
const innerType = typeName.substring(prefix.length);
// Java 基础类型则转为 JS 基础类型
if (typeMapping[innerType]) {
return typeMapping[innerType];
}

if (openapi.components?.schemas) {
// schemas 中已存在的自定义类型,使用原类型名称
const ref = openapi.components.schemas[innerType as a];
type a = keyof typeof openapi.components.schemas
if (ref) {
return innerType;
}
}

// 基础类型转换为小写开头
return innerType.charAt(0).toLowerCase() + innerType.slice(1);
}

const username: string = 'aaa';
const password: string = '111';
async function getOpenApiDoc() {
const res = await axios.get<OpenApi>('https://aaa.com/v3/api-docs/app', {
headers: {
'Authorization': `Basic ${btoa(`${username}:${password}`)}`
}
});
const openapi = res.data
tsContent += generateType(openapi);
tsContent += generateApi(openapi);

const outputFile = 'generated-api.ts';
fs.writeFile(outputFile, tsContent, (err) => {
if (err) {
console.error(`写入文件时出错: ${err}`);
} else {
console.log(`TypeScript Axios 接口已生成到 ${outputFile}`);
}
});
}

getOpenApiDoc();