メインコンテンツまでスキップ

OpenAPI Generator を活用した TypeScript 型定義の自動生成と整形

· 約5分
Macshion
Front-End Engineer

はじめに

OpenAPI 仕様を活用して API クライアントコードや型定義を自動生成することで、手作業によるミスを減らし、開発効率を向上させることができます。本記事では、openapi-generator-cli を使用して TypeScript 型定義を自動生成し、さらに生成されたコードを整形・最適化するスクリプトを紹介します。

OpenAPI Generator の実行

以下の docker run コマンドを使用して、openapi-generator-cli を実行し、TypeScript の型定義を生成します。

自動生成の実行

./openapi-generator.sh

openapi-generator.sh

#!/bin/bash
docker run --rm -v "$(dirname ${PWD}):/local" -w "/local" \
openapitools/openapi-generator-cli:v6.6.0 generate \
-g typescript-fetch \
-i ./api/reference/api.yaml \
-o ./front/src/types \
--skip-validate-spec \
--inline-schema-name-defaults arrayItemSuffix=,mapItemSuffix= \
--global-property=apiTests=false,modelTests=false,apiDocs=false,modelDocs=false,models,apis \
--additional-properties=typescriptThreePlus=true,enumPropertyNaming=UPPERCASE,supportsES6=true,withInterfaces=true,useSingleRequestParameter=true \
--global-property models=

このコマンドにより、api.yaml から TypeScript 型定義 (apismodels) を ./front/src/types ディレクトリに生成します。

生成されたコードの整形

生成されたコードには、不要なコードや冗長なコードが含まれている場合があります。以下の Node.js スクリプトを使用して、コードを整形・最適化します。

整形スクリプトの実行

node ./openapi-format.js

openapi-format.js の概要

このスクリプトでは、以下の処理を実行します。

  1. 非同期関数の削除 (removeAsyncFunctions)
  2. 継承クラスの削除 (removeClassesWithBraces)
  3. 未使用の import 文の削除 (removeUnusedImports)
  4. コメントの削除 (/\*\*[\s\S]*?\*/)
  5. 不要な空行の削除 (removeExtraBlankLines)

openapi-format.jsのコード

const fs = require("fs");
const path = require("path");

function processDirectory(dir) {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory()) {
processDirectory(filePath);
} else if (file.endsWith(".ts")) {
processFile(filePath);
}
});
}

// 生成したapi型定義のファイルをフォーマットする
function processFile(filePath) {
const content = fs.readFileSync(filePath, "utf8");
let updatedContent = removeAsyncFunctions(content);
updatedContent = removeClassesWithBraces(updatedContent);
updatedContent = updatedContent.replace(/\/\*\*[\s\S]*?\*\//g, "");
updatedContent = removeUnusedImports(updatedContent);
updatedContent = removeExtraBlankLines(updatedContent);
fs.writeFileSync(filePath, updatedContent, "utf8");
console.log(`Format Processed: ${filePath}`);
}

// 一般的な括弧スキップ関数(ユーティリティ関数)
function skipBraces(content, startIndex) {
let braceCount = 0;

for (let i = startIndex; i < content.length; i++) {
if (content[i] === "{") braceCount++;
else if (content[i] === "}") braceCount--;

if (braceCount === 0) {
return i + 1;
}
}

return content.length;
}

function removeClassesWithBraces(content) {
let result = "";
let index = 0;

while (index < content.length) {
const classMatch = content
.slice(index)
.match(/export\s+class\s+\w+\s+extends\s+\w+/);
if (!classMatch) {
result += content.slice(index);
break;
}

result += content.slice(index, index + classMatch.index);
index += classMatch.index;

const startBraceIndex = content.indexOf("{", index);
if (startBraceIndex === -1) break;

index = skipBraces(content, startBraceIndex);
}

return result;
}

function removeAsyncFunctions(content) {
let result = "";
let index = 0;

while (index < content.length) {
const asyncMatch = content.slice(index).match(/async\s+\w+\s*\(/);
if (!asyncMatch) {
result += content.slice(index);
break;
}

result += content.slice(index, index + asyncMatch.index);
index += asyncMatch.index;

const startBraceIndex = content.indexOf("{", index);
if (startBraceIndex === -1) break;

index = skipBraces(content, startBraceIndex);
}

return result;
}

function removeUnusedImports(content) {
const importStatements =
content.match(/import\s+[\s\S]+?from\s+['"][^'"]+['"];?/g) || [];
let updatedContent = content;

importStatements.forEach((importStatement) => {
const matches = importStatement.match(/{([\s\S]*?)}/);
if (!matches) return;

const importedItems = matches[1]
.split(",")
.map((item) => item.trim())
.filter(Boolean);

const unusedItems = importedItems.filter((item) => {
const regex = new RegExp(`\\b${item}\\b`, "g");
const usageCount = (
updatedContent.replace(importStatement, "").match(regex) || []
).length;
return usageCount === 0; // 未使用
});

// すべての変数が未使用の場合、import 文全体を削除します
if (unusedItems.length === importedItems.length) {
updatedContent = updatedContent.replace(importStatement, "");
} else if (unusedItems.length > 0) {
// 一部の変数が未使用の場合、これらの変数を削除します
const usedItems = importedItems.filter(
(item) => !unusedItems.includes(item)
);
const newImport = `import { ${usedItems.join(", ")} } from ${
importStatement.split("from")[1]
}`;
updatedContent = updatedContent.replace(importStatement, newImport);
}
});
return updatedContent;
}

function removeExtraBlankLines(content) {
return content.replace(/\n\s*\n+/g, "\n");
}

// 生成されたコードのターゲットディレクトリパスに置き換えます
processDirectory("./src/types/apis");
processDirectory("./src/types/models");

まとめ

本記事では、openapi-generator-cli を使用して TypeScript 型定義を自動生成し、さらに整形スクリプトで最適化する手法を紹介しました。このワークフローを導入することで、開発効率を向上させ、より保守性の高いコードを維持することができます。

ポイントまとめ

  • OpenAPI Generator を利用して TypeScript 型定義を自動生成
  • Docker を活用した環境構築
  • Node.js スクリプトによる自動整形
  • 冗長なコードを削除し、コードの可読性を向上