iTranslated by AI
[TypeScript] Efficient Development with Perfect Autocompletion from OpenAPI v3 Definitions
TypeScript Input Completion
TypeScript, when used with a development environment like VSCode, allows you to narrow down candidates through input completion for various elements. The presence or absence of this feature makes a world of difference in development efficiency. Given that, it's only human to want to make everything a target for completion. This time, based on an OpenAPI v3 definition file, we will fully enable completion for accessing a RestAPI.
Advantages of OpenAPI (Swagger)
By creating a RestAPI definition with OpenAPI, you can obtain the following advantages:
- Share documentation with the team
- Set up mock servers
- Generate type information for various languages
We will use TypeScript this time, but since it is a common specification format, it is language-agnostic.
How to Utilize it in TypeScript
Several packages are available to generate TypeScript types from OpenAPI definition information. By utilizing them, you can to some extent avoid the situation of manually writing type information for RestAPI input/output.
This time, we will use a package called openapi-typescript to generate type information.
Main Packages Used This Time
-
openapi-typescript
A package to convert OpenAPI definition information into a format usable in TypeScript. -
request-restapi
A package that uses the TypeScript type information output by openapi-typescript to access a RestAPI (I created this myself).
How to Obtain Type Information
For the sample, we will convert GitHub's OpenAPI definition information into TypeScript definitions.
npx openapi-typescript https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/ghes-3.0/ghes-3.0.yaml > github.d.ts
With this command, TypeScript type information is generated in github.d.ts. This file alone is 1.2MB just for the definition information. In the sample, we will use this to verify input completion in TypeScript.
Program
This is a sample for accessing the GitHub RestAPI.
https://github.com/SoraKumo001/request-restapi-test
You will need a token for authentication, so you should create one at https://github.com/settings/tokens beforehand and put TOKEN=YOUR_GITHUB_ACCESS_TOKEN in your .env file. No permissions need to be attached to the token you create.
import { Rest } from "request-restapi";
import { paths } from "./github";
import env from "dotenv";
env.config();
const token = process.env.TOKEN;
const rest = new Rest<paths>({ baseUrl: "https://api.github.com", token });
//Display user name
(async () => {
const resultUser = await rest.request({
path: "/user",
method: "get",
});
// Once the code is identified, the body is determined
if (resultUser.code === 200) {
const { body } = resultUser;
console.log(`UserName: ${body.name}`);
} else {
console.error(resultUser);
}
console.log("---------");
// View repository list
for (let page = 1; page < 100; page++) {
const result = await rest.request({
path: "/user/repos",
method: "get",
query: { page },
});
if (result.code === 200) {
const { body } = result;
if (body.length === 0) break;
body.forEach((value) => {
console.log(value.name);
});
} else {
console.error(result);
break;
}
}
console.log("---------");
// View specific repository information
const resultRepo = await rest.request({
path: "/repos/{owner}/{repo}",
params: { owner: "SoraKumo001", repo: "request-restapi" },
method: "get",
});
if (resultRepo.code === 200) {
const { body, headers } = resultRepo;
console.log(body);
console.log(headers);
} else {
console.error(resultRepo);
}
})();
Everything is Completed
Looking only at the program, it might seem like it's just providing functionality to access a RestAPI. What's different from general packages is that the type information is strictly locked down by request-restapi.
-
Path input completion
-
After the path is decided, method candidates are narrowed down
-
Information for path parameters is also displayed
-
All parameters to be set in the body also appear
-
Naturally, type information is also provided for return values
-
Identifying the code narrows down the type of data
We are using the GitHub API definition this time, but as long as you have an OpenAPI v3 specification definition file, it can be used in the same way for other APIs.
The Hellish Scenery Behind This Type Definition
request-restapi is built as follows.
Most of the program is occupied by code for calculating types.
import fetch from "node-fetch";
interface Props {
baseUrl: string;
authKey?: string;
token?: string;
}
export class Rest<T> {
private readonly baseUrl: string;
private readonly authKey: string;
private readonly token?: string;
constructor({ baseUrl, token, authKey = "Bearer" }: Props) {
this.baseUrl = baseUrl;
this.authKey = authKey;
this.token = token;
}
public request<
P extends T,
PATH extends keyof P,
METHOD extends keyof P[PATH],
RET extends P[PATH][METHOD] extends { responses: infer res }
? {
[P in keyof res]: {
code: P;
headers: Headers;
body: res[P] extends { schema: infer R }
? R
: res[P] extends { content: { "application/json": infer R2 } }
? R2
: Blob;
};
} extends {
[P in string]: infer R;
}
? R
: never
: never
>({
method,
path,
params,
headers,
query,
body,
token,
}: {
method: METHOD;
path: PATH;
params?: P[PATH][METHOD] extends { parameters: { path: infer R } }
? R extends { [M in keyof R]: R[M] }
? R
: never
: P[PATH] extends { parameters: { path: infer R } }
? R extends { [M in keyof R]: R[M] }
? R
: never
: never;
headers?: P[PATH][METHOD] extends { parameters: { header: infer R } }
? R extends { [_ in string]: unknown }
? R
: never
: never;
query?: P[PATH][METHOD] extends { parameters: { query: infer R } }
? R extends { [_ in string]: unknown }
? R
: never
: never;
body?: P[PATH][METHOD] extends {
requestBody: { content: { [key: string]: infer R } };
}
? R
: never;
token?: string;
}): Promise<RET> {
const regularParam = params
? Object.entries(params).reduce(
(p, [key, value]) =>
p.replace(new RegExp(`\\{${key}\\}`), String(value)),
path as string
)
: path;
const queryParam = query
? Object.entries(query)
.reduce((a, [key, value]) => `${a}${key}=${value}&`, "?")
.trimEnd()
: "";
return fetch(this.baseUrl + regularParam + queryParam, {
method: (method as string).toUpperCase(),
headers: {
"Content-Type": "application/json",
...(typeof headers === "object" ? headers : {}),
...(token || this.token
? { Authorization: `${this.authKey} ${token || this.token}` }
: {}),
},
body: body && JSON.stringify(body),
}).then(
async (res) =>
({
code: res.status,
headers: res.headers,
body: await res.json().catch(async () => await res.blob()),
} as RET)
);
}
}
I'm Starting to Think Anything is Possible if You Try Hard Enough with TypeScript
TypeScript is just that kind of language.
Discussion