TypeScript ビルドツール完全ガイド
tsc, esbuild, SWC, Vite を比較し、プロジェクトに最適なビルドパイプラインを構築する
TypeScript ビルドツール完全ガイド
tsc, esbuild, SWC, Vite を比較し、プロジェクトに最適なビルドパイプラインを構築する
この章で学ぶこと
- 各ビルドツールの特性 -- tsc, esbuild, SWC, Vite それぞれの設計思想、速度、機能の違い
- ビルドパイプラインの設計 -- 型チェックとトランスパイルの分離、開発/本番環境の構成パターン
- 移行とチューニング -- 既存プロジェクトのビルド高速化と、ツール間の移行手順
- モノレポでのビルド戦略 -- Turborepo, Nx との連携、キャッシュ活用
- ライブラリのビルドとパッケージング -- tsup, unbuild, ESM/CJS デュアル出力
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- tsconfig.json 完全ガイド の内容を理解していること
1. ビルドツール全体像
1-1. TypeScript ビルドの 2 つの役割
TypeScript ビルドの分離:
.ts ファイル
|
+----+----+
| |
v v
型チェック トランスパイル
(tsc) (esbuild / SWC / Vite)
| |
v v
エラー報告 .js ファイル
| |
+----+----+
|
v
デプロイ可能なコード
ポイント: 型チェックとトランスパイルは分離できる
tsc は型チェックのみ (--noEmit) に使い、
高速トランスパイラで JS を生成するのが現代のベストプラクティス
1-2. 速度比較(10万行プロジェクト目安)
ビルド速度比較 (相対値):
tsc ████████████████████████████████████ 30s
webpack+ts ████████████████████████████ 25s
Rollup+ts ████████████████████ 18s
Vite (dev) ██ 2s (HMR)
esbuild █ 0.5s
SWC █ 0.4s
※ tsc は型チェック込み、他は型チェックなし(トランスパイルのみ)
※ esbuild と SWC は型チェックなしの純粋なトランスパイル速度
型チェック + トランスパイルの合計時間:
tsc only ████████████████████████████████████ 30s
tsc + esbuild ████████████████ + █ 15.5s
tsc + SWC ████████████████ + █ 15.4s
tsc + Vite ████████████████ + ██ 17s
→ tsc の型チェック時間は変わらないが、
トランスパイルは高速ツールに任せることで
開発サーバーの起動やHMRが劇的に速くなる
1-3. ツールの選定フローチャート
ビルドツール選定ガイド:
Q1: フロントエンドアプリ?
├── Yes → Vite を使う
│ ├── React → @vitejs/plugin-react-swc
│ ├── Vue → @vitejs/plugin-vue
│ └── Svelte → @sveltejs/vite-plugin-svelte
└── No
Q2: npm ライブラリ?
├── Yes → tsup (esbuild ベース) or unbuild
└── No
Q3: Node.js バックエンド?
├── Yes
│ ├── 開発 → tsx (esbuild ベース)
│ └── 本番 → esbuild でバンドル
└── No
Q4: モノレポ?
├── Yes → Turborepo + 各パッケージのツール
└── → プロジェクトに合ったツールを選択
2. tsc(TypeScript Compiler)
2-1. 基本コマンド
// package.json scripts
{
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch",
"build:project": "tsc --build",
"build:clean": "tsc --build --clean"
}
}
// tsc の主要フラグ
// tsc → tsconfig.json に従ってビルド
// tsc --noEmit → 型チェックのみ(ファイル出力なし)
// tsc --watch → ファイル変更を監視して再ビルド
// tsc --build → プロジェクト参照を含むインクリメンタルビルド
// tsc --declaration → .d.ts ファイルを生成
// tsc --project tsconfig.test.json → 指定した設定ファイルを使用
// tsc --extendedDiagnostics → パフォーマンス診断情報を出力
// tsc --generateTrace ./trace → パフォーマンストレースを生成2-2. インクリメンタルビルド
// tsconfig.json
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./.tsbuildinfo"
}
}
// 前回のビルド情報を .tsbuildinfo に保存し、
// 変更のあったファイルのみ再コンパイル
// → 2回目以降のビルドが大幅に高速化インクリメンタルビルドの効果(実測例):
初回: 30.2s (全ファイル)
2回目: 4.1s (変更なし、キャッシュ検証のみ)
3回目: 6.8s (10ファイル変更)
クリーン後: 30.5s (キャッシュなし)
→ 2回目以降は 70-85% の高速化
.tsbuildinfo ファイルの内容:
- 各ファイルのハッシュ
- 依存関係グラフ
- コンパイラオプションのスナップショット
- 出力ファイルのシグネチャ
2-3. tsc と他ツールの併用パターン
// パターン1: tsc (型チェック) + esbuild (トランスパイル)
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js",
"prebuild": "npm run typecheck"
}
}
// パターン2: tsc (型定義生成) + esbuild (JS生成)
{
"scripts": {
"build:types": "tsc --emitDeclarationOnly --declaration --declarationMap",
"build:js": "esbuild src/index.ts --bundle --outfile=dist/index.js",
"build": "npm run build:types && npm run build:js"
}
}
// パターン3: 並列実行(concurrently を使用)
{
"scripts": {
"dev": "concurrently \"tsc --noEmit --watch\" \"tsx watch src/index.ts\"",
"build": "concurrently \"tsc --noEmit\" \"esbuild src/index.ts --bundle --outfile=dist/index.js\""
}
}2-4. tsc --build(プロジェクト参照ビルド)
# モノレポでのビルド
tsc --build # 全パッケージをビルド
tsc --build packages/shared # shared とその依存のみ
tsc --build --watch # ウォッチモード
tsc --build --verbose # 詳細出力
tsc --build --dry # ドライラン
tsc --build --clean # ビルド成果物を削除
tsc --build --force # キャッシュを無視して全ビルドtsc --build の動作:
1. 依存グラフを解析
shared → frontend (shared に依存)
→ backend (shared に依存)
→ e2e (frontend, backend に依存)
2. トポロジカルソートで順序決定
shared → [frontend, backend] → e2e
3. 各パッケージを順番にビルド
- .tsbuildinfo でキャッシュチェック
- 変更がなければスキップ
- 変更があれば再ビルド
4. 下流パッケージの再ビルド判定
- shared が変更 → frontend, backend, e2e 全て再ビルド
- frontend のみ変更 → frontend, e2e のみ再ビルド
3. esbuild
3-1. 基本セットアップ
// esbuild.config.ts
import * as esbuild from "esbuild";
// シンプルなビルド
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/index.js",
platform: "node",
target: "node20",
format: "esm",
sourcemap: true,
minify: process.env.NODE_ENV === "production",
});
// 複数エントリーポイント
await esbuild.build({
entryPoints: ["src/index.ts", "src/worker.ts"],
bundle: true,
outdir: "dist",
splitting: true, // コード分割(ESM のみ)
format: "esm",
platform: "node",
target: "node20",
external: ["pg", "redis"], // バンドルしないパッケージ
});
// ブラウザ向けビルド
await esbuild.build({
entryPoints: ["src/app.ts"],
bundle: true,
outfile: "dist/app.js",
platform: "browser",
target: ["chrome100", "firefox100", "safari16"],
format: "esm",
sourcemap: true,
minify: true,
// CSS もバンドル
loader: {
".png": "file",
".svg": "dataurl",
".css": "css",
},
// 環境変数の埋め込み
define: {
"process.env.NODE_ENV": '"production"',
"import.meta.env.VITE_API_URL": '"https://api.example.com"',
},
});3-2. esbuild プラグイン
import * as esbuild from "esbuild";
import { readFile } from "fs/promises";
// カスタムプラグインの作成
const envPlugin: esbuild.Plugin = {
name: "env-plugin",
setup(build) {
// .env ファイルを読み込んで定義に変換
build.onResolve({ filter: /^env$/ }, (args) => ({
path: args.path,
namespace: "env-ns",
}));
build.onLoad({ filter: /.*/, namespace: "env-ns" }, async () => {
const envFile = await readFile(".env", "utf-8");
const env: Record<string, string> = {};
for (const line of envFile.split("\n")) {
const [key, ...valueParts] = line.split("=");
if (key && valueParts.length > 0) {
env[key.trim()] = valueParts.join("=").trim();
}
}
return {
contents: JSON.stringify(env),
loader: "json",
};
});
},
};
// リビルド通知プラグイン
const notifyPlugin: esbuild.Plugin = {
name: "notify-plugin",
setup(build) {
let start: number;
build.onStart(() => {
start = Date.now();
console.log("Build started...");
});
build.onEnd((result) => {
const elapsed = Date.now() - start;
if (result.errors.length > 0) {
console.error(`Build failed in ${elapsed}ms with ${result.errors.length} errors`);
} else {
console.log(`Build completed in ${elapsed}ms`);
if (result.warnings.length > 0) {
console.warn(` ${result.warnings.length} warnings`);
}
}
});
},
};
// Node.js の外部パッケージを自動検出するプラグイン
const nodeExternalsPlugin: esbuild.Plugin = {
name: "node-externals",
setup(build) {
// node_modules のパッケージを全て external にする
build.onResolve({ filter: /^[^./]/ }, (args) => ({
path: args.path,
external: true,
}));
},
};
// プラグインの使用
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/index.js",
platform: "node",
format: "esm",
plugins: [envPlugin, notifyPlugin, nodeExternalsPlugin],
});3-3. 開発サーバー
// esbuild の watch + serve
const ctx = await esbuild.context({
entryPoints: ["src/index.ts"],
bundle: true,
outdir: "dist",
platform: "node",
format: "esm",
sourcemap: true,
plugins: [
{
name: "rebuild-notify",
setup(build) {
build.onEnd((result) => {
console.log(
`Build finished: ${result.errors.length} errors`
);
});
},
},
],
});
await ctx.watch(); // ファイル変更を監視して自動リビルド
// フロントエンド用の開発サーバー
const serveCtx = await esbuild.context({
entryPoints: ["src/app.ts"],
bundle: true,
outdir: "public/dist",
platform: "browser",
format: "esm",
sourcemap: true,
});
// 開発サーバーを起動
const { host, port } = await serveCtx.serve({
servedir: "public",
port: 3000,
});
console.log(`Dev server running at http://${host}:${port}`);3-4. esbuild の制限事項
esbuild がサポートしない TypeScript 機能:
1. 型チェック
→ tsc --noEmit を別途実行
2. const enum(cross-file inlining)
→ isolatedModules: true で回避
→ 通常の enum として扱われる
3. デコレータ(experimentalDecorators)
→ ECMAScript 標準デコレータ (Stage 3) はサポート
→ 旧式の TypeScript デコレータは --loader=ts で部分サポート
4. 宣言ファイル(.d.ts)生成
→ tsc --emitDeclarationOnly を併用
5. 一部のtsconfig オプション
→ emitDecoratorMetadata: 非サポート
→ paths: 限定的サポート(プラグインで対応可能)
対策: esbuild は JS 生成のみに使い、
型関連は全て tsc に任せる
3-5. package.json 設定
{
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=esm",
"build:prod": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=esm --minify --sourcemap",
"dev": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=esm --watch",
"typecheck": "tsc --noEmit"
}
}4. SWC
4-1. 基本セットアップ
// .swcrc
{
"$schema": "https://swc.rs/schema.json",
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": true,
"dynamicImport": true
},
"target": "es2022",
"transform": {
"decoratorVersion": "2022-03"
},
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"module": {
"type": "es6",
"strict": true,
"noInterop": false
},
"sourceMaps": true,
"minify": false
}// package.json
{
"scripts": {
"build": "swc src -d dist --strip-leading-paths",
"build:watch": "swc src -d dist --strip-leading-paths --watch",
"dev": "swc src -d dist --strip-leading-paths --watch",
"typecheck": "tsc --noEmit"
}
}4-2. SWC + Node.js 実行
// @swc-node/register で直接 .ts を実行
{
"scripts": {
"start": "node --import @swc-node/register/esm src/index.ts",
"dev": "node --import @swc-node/register/esm --watch src/index.ts"
}
}4-3. SWC の minification
// .swcrc に minify 設定を追加
{
"jsc": {
"parser": { "syntax": "typescript" },
"target": "es2022",
"minify": {
"compress": {
"dead_code": true,
"drop_console": true,
"drop_debugger": true,
"passes": 2,
"unused": true
},
"mangle": {
"toplevel": true,
"keep_classnames": false,
"keep_fnames": false
}
}
},
"minify": true
}4-4. SWC vs esbuild の詳細比較
SWC と esbuild の違い:
SWC (Rust 製):
├── トランスパイルのみ(バンドルは swcpack で実験的)
├── decorators の完全サポート(emitDecoratorMetadata 含む)
├── Next.js / Parcel に組み込まれている
├── プラグインシステム(Wasm / Rust)
└── minification サポート
esbuild (Go 製):
├── トランスパイル + バンドル
├── HTTP サーバー内蔵
├── Tree-shaking サポート
├── コード分割サポート
└── プラグインシステム(JavaScript)
選択基準:
- バンドルが必要 → esbuild
- NestJS / Angular(旧式デコレータ) → SWC
- Next.js → SWC(組込み)
- 汎用トランスパイル → どちらでも
5. Vite
5-1. 基本設定
Vite の開発/本番フロー:
開発 (dev):
+----------+ +---------+ +----------+
| .ts ファイル| --> | esbuild | --> | ブラウザ |
| (ソース) | | (変換) | | (ESM) |
+----------+ +---------+ +----------+
↑ HMR(ミリ秒単位で更新)
本番 (build):
+----------+ +---------+ +----------+
| .ts ファイル| --> | Rollup | --> | バンドル |
| (ソース) | | + SWC | | (最適化) |
+----------+ +---------+ +----------+
Vite 6.x 以降:
+----------+ +---------+ +----------+
| .ts ファイル| --> | Rolldown| --> | バンドル |
| (ソース) | | (Rust) | | (高速) |
+----------+ +---------+ +----------+
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc"; // SWC 版 React プラグイン
export default defineConfig({
plugins: [react()],
build: {
target: "es2022",
outDir: "dist",
sourcemap: true,
// Rollup のオプション
rollupOptions: {
output: {
// 手動チャンク分割
manualChunks: {
vendor: ["react", "react-dom"],
ui: ["@radix-ui/react-dialog", "@radix-ui/react-dropdown-menu"],
},
// チャンクファイル名のフォーマット
chunkFileNames: "assets/[name]-[hash].js",
entryFileNames: "assets/[name]-[hash].js",
assetFileNames: "assets/[name]-[hash].[ext]",
},
},
// チャンクサイズ警告の閾値
chunkSizeWarningLimit: 500,
// CSS のコード分割
cssCodeSplit: true,
},
resolve: {
alias: {
"@": "/src",
"@components": "/src/components",
"@hooks": "/src/hooks",
"@utils": "/src/utils",
},
},
server: {
port: 3000,
strictPort: true,
// プロキシ設定
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
// 環境変数のプレフィックス
envPrefix: "VITE_",
});5-2. Vite プラグイン
// よく使われる Vite プラグイン
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tsconfigPaths from "vite-tsconfig-paths";
import checker from "vite-plugin-checker";
import { compression } from "vite-plugin-compression2";
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
// React (SWC ベース、高速)
react(),
// tsconfig.json の paths を自動解決
tsconfigPaths(),
// 開発中にリアルタイム型チェック + ESLint
checker({
typescript: true,
eslint: {
lintCommand: 'eslint "./src/**/*.{ts,tsx}"',
},
}),
// Gzip / Brotli 圧縮
compression({
algorithm: "gzip",
threshold: 1024,
}),
compression({
algorithm: "brotliCompress",
threshold: 1024,
}),
// バンドルサイズの可視化
visualizer({
filename: "dist/stats.html",
open: false,
gzipSize: true,
brotliSize: true,
}),
],
});5-3. ライブラリモード
// vite.config.ts -- ライブラリとしてビルド
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
plugins: [
dts({
rollupTypes: true, // 型定義を1ファイルにバンドル
insertTypesEntry: true, // package.json の types を自動設定
tsconfigPath: "./tsconfig.build.json",
}),
],
build: {
lib: {
entry: "src/index.ts",
name: "MyLib",
formats: ["es", "cjs"],
fileName: (format) => `index.${format === "es" ? "mjs" : "cjs"}`,
},
rollupOptions: {
// ピア依存はバンドルしない
external: ["react", "react-dom", "react/jsx-runtime"],
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
},
},
},
// ソースマップ生成
sourcemap: true,
// minify しない(ライブラリの場合)
minify: false,
},
});5-4. SSR / バックエンド
// vite.config.ts -- Node.js バックエンド
import { defineConfig } from "vite";
export default defineConfig({
build: {
ssr: true,
target: "node20",
outDir: "dist",
rollupOptions: {
input: "src/server.ts",
output: {
format: "esm",
entryFileNames: "server.js",
},
},
},
ssr: {
noExternal: true, // 全ての依存をバンドル
// noExternal: ["specific-pkg"], // 特定のみバンドル
},
});
// Vite + Express の SSR 設定例
// vite.config.ts
export default defineConfig({
build: {
ssr: true,
rollupOptions: {
input: {
server: "src/server.ts",
entry: "src/entry-server.tsx",
},
},
},
});5-5. 環境変数の管理
// .env ファイルの読み込み順序:
// .env # 常に読み込み
// .env.local # 常に読み込み(.gitignore 推奨)
// .env.[mode] # 指定モードで読み込み
// .env.[mode].local # 指定モードで読み込み(.gitignore 推奨)
// .env
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
// .env.development
VITE_API_URL=http://localhost:8080
VITE_DEBUG=true
// .env.production
VITE_API_URL=https://api.production.com
VITE_DEBUG=false
// src/vite-env.d.ts -- 型定義
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
readonly VITE_DEBUG: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// 使用例
const apiUrl = import.meta.env.VITE_API_URL; // 型: string
const isDev = import.meta.env.DEV; // 型: boolean
const isProd = import.meta.env.PROD; // 型: boolean
const mode = import.meta.env.MODE; // 型: string6. tsx -- TypeScript Execute
6-1. 基本的な使い方
# インストール
npm install -D tsx
# TypeScript ファイルを直接実行
npx tsx src/index.ts
# ウォッチモード
npx tsx watch src/index.ts
# ESM として実行
npx tsx --esm src/index.ts
# REPL
npx tsx// package.json
{
"scripts": {
"start": "tsx src/index.ts",
"dev": "tsx watch src/index.ts",
"script": "tsx scripts/seed.ts"
}
}6-2. tsx vs ts-node vs node --loader
TypeScript 実行ランナー比較:
tsx (esbuild ベース):
├── 起動時間: 非常に速い
├── 型チェック: なし
├── ESM サポート: あり
├── tsconfig paths: 自動解決
└── 設定: ほぼ不要
ts-node (tsc ベース):
├── 起動時間: 遅い
├── 型チェック: あり(swc モード時はなし)
├── ESM サポート: 要設定
├── tsconfig paths: tsconfig-paths 必要
└── 設定: 多い
node --experimental-strip-types (Node.js 23+):
├── 起動時間: 最速
├── 型チェック: なし
├── ESM サポート: あり
├── tsconfig paths: 非サポート
└── 設定: 不要
└── 注意: enum, namespace 等は非サポート
node --import @swc-node/register/esm:
├── 起動時間: 速い
├── 型チェック: なし
├── ESM サポート: あり
├── tsconfig paths: 要設定
└── 設定: 少ない
7. tsup / unbuild -- ライブラリビルダー
7-1. tsup の設定
// tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
// エントリーポイント
entry: ["src/index.ts", "src/utils/index.ts"],
// 出力形式(ESM + CJS)
format: ["esm", "cjs"],
// 型定義ファイル生成
dts: true,
// ソースマップ
sourcemap: true,
// クリーンビルド
clean: true,
// 外部パッケージ(バンドルしない)
external: ["react", "react-dom"],
// Tree-shaking
treeshake: true,
// TypeScript のターゲット
target: "es2020",
// 出力ディレクトリ
outDir: "dist",
// コード分割
splitting: true,
// minify
minify: false,
});// package.json(デュアル ESM/CJS パッケージ)
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./utils": {
"import": {
"types": "./dist/utils/index.d.ts",
"default": "./dist/utils/index.js"
},
"require": {
"types": "./dist/utils/index.d.cts",
"default": "./dist/utils/index.cjs"
}
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build"
}
}7-2. unbuild の設定
// build.config.ts
import { defineBuildConfig } from "unbuild";
export default defineBuildConfig({
entries: ["src/index"],
declaration: true,
clean: true,
rollup: {
emitCJS: true,
inlineDependencies: true,
},
// 自動 externals 検出
externals: ["react"],
});7-3. tsup vs unbuild の比較
ライブラリビルダー比較:
tsup:
├── エンジン: esbuild
├── 速度: 非常に速い
├── 設定: シンプル
├── DTS: esbuild + tsc (ハイブリッド)
├── Tree-shaking: esbuild
└── 推奨: 小〜中規模ライブラリ
unbuild:
├── エンジン: Rollup
├── 速度: 速い
├── 設定: シンプル
├── DTS: rollup-plugin-dts
├── Tree-shaking: Rollup(高品質)
└── 推奨: Nuxt エコシステム
Vite lib mode:
├── エンジン: Rollup
├── 速度: 速い
├── 設定: Vite の知識が必要
├── DTS: vite-plugin-dts
├── Tree-shaking: Rollup(高品質)
└── 推奨: Vite プロジェクトのライブラリ
tsc:
├── エンジン: TypeScript Compiler
├── 速度: 遅い
├── 設定: tsconfig.json
├── DTS: ネイティブ(最も正確)
├── Tree-shaking: なし
└── 推奨: 型定義の正確さが最重要の場合
8. モノレポでのビルド戦略
8-1. Turborepo
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"inputs": ["src/**", "tsconfig.json"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"lint": {},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}# Turborepo でのビルド
turbo build # 全パッケージをビルド(キャッシュ活用)
turbo build --filter=web # web パッケージのみ
turbo build --force # キャッシュを無視
turbo dev # 全パッケージの開発サーバー
# リモートキャッシュ(チーム間でキャッシュ共有)
turbo login
turbo link
turbo build # リモートキャッシュを使用8-2. モノレポのパッケージ構成例
monorepo/
├── apps/
│ ├── web/ (Next.js)
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── api/ (Node.js + esbuild)
│ ├── package.json
│ └── tsconfig.json
├── packages/
│ ├── shared/ (tsup でビルド)
│ │ ├── package.json
│ │ ├── tsup.config.ts
│ │ └── tsconfig.json
│ ├── ui/ (Vite lib mode)
│ │ ├── package.json
│ │ ├── vite.config.ts
│ │ └── tsconfig.json
│ └── config-ts/ (共有 tsconfig)
│ ├── base.json
│ ├── nextjs.json
│ ├── node.json
│ └── library.json
├── turbo.json
├── package.json
└── tsconfig.json
9. 最適なパイプライン設計
9-1. フロントエンド(React / Vue)
推奨パイプライン:
開発: Vite dev server (esbuild でトランスパイル)
型チェック: tsc --noEmit (バックグラウンド or CI)
本番ビルド: Vite build (Rollup + minify)
テスト: Vitest (Vite と設定共有)
lint: ESLint + Prettier
package.json:
{
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
}
9-2. Node.js バックエンド
推奨パイプライン:
開発: tsx (esbuild ベースの ts-node 代替)
型チェック: tsc --noEmit
本番ビルド: esbuild (バンドル + minify)
テスト: Vitest
lint: ESLint
package.json:
{
"dev": "tsx watch src/index.ts",
"build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=esm --minify",
"start": "node dist/index.js",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
9-3. npm ライブラリ
推奨パイプライン:
開発: tsx で実行テスト
型チェック: tsc --noEmit
ビルド: tsup (esbuild ベース、ESM + CJS 両出力)
型定義: tsup --dts (tsc を内部使用)
テスト: Vitest
公開: npm publish (prepublishOnly で build)
package.json:
{
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"test": "vitest",
"prepublishOnly": "npm run build && npm run typecheck"
}
9-4. フルスタック(Next.js + tRPC)
推奨パイプライン:
フレームワーク: Next.js (SWC 組込み)
API: tRPC (型共有)
DB: Prisma (型生成)
バリデーション: zod
テスト: Vitest + Playwright
package.json:
{
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"test": "vitest",
"test:e2e": "playwright test",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"typecheck": "tsc --noEmit",
"postinstall": "prisma generate"
}
比較表
ビルドツール総合比較
| 特性 | tsc | esbuild | SWC | Vite | tsup |
|---|---|---|---|---|---|
| 言語 | TypeScript | Go | Rust | JS (esbuild/Rollup) | JS (esbuild) |
| 型チェック | あり | なし | なし | なし | なし |
| トランスパイル速度 | 遅い | 最速級 | 最速級 | 速い (esbuild) | 速い (esbuild) |
| バンドル | なし | あり | 実験的 | あり (Rollup) | あり |
| Tree-shaking | なし | あり | なし | あり | あり |
| HMR | なし | 簡易 | なし | 優秀 | なし |
| プラグイン | なし | あり | あり | 豊富 | esbuild互換 |
| 設定の簡単さ | 中 | 高 | 中 | 高 | 最高 |
| .d.ts 生成 | ネイティブ | なし | なし | プラグイン | あり |
| CSS バンドル | なし | あり | なし | あり | なし |
| コード分割 | なし | あり(ESM) | なし | あり | あり |
用途別推奨ツール
| 用途 | 推奨 | 理由 |
|---|---|---|
| React / Vue SPA | Vite | HMR, プラグイン充実 |
| Next.js | (組込みSWC) | フレームワーク統合 |
| Node.js API | esbuild / tsx | 高速、シンプル |
| npm ライブラリ | tsup (esbuild) | ESM/CJS 両出力、DTS生成 |
| モノレポ | Turborepo + Vite/esbuild | キャッシュ, 並列ビルド |
| Deno | (組込み) | 設定不要 |
| Bun | (組込み) | 設定不要、最速 |
| Cloudflare Workers | wrangler (esbuild) | Edge 最適化 |
| Electron | Vite + electron-builder | HMR + パッケージング |
パフォーマンス指標(実測参考値)
| ツール | 1000ファイル | 5000ファイル | 10000ファイル |
|---|---|---|---|
| tsc (初回) | 3s | 12s | 30s |
| tsc (インクリメンタル) | 0.5s | 2s | 5s |
| esbuild (バンドル) | 0.1s | 0.3s | 0.5s |
| SWC (トランスパイル) | 0.08s | 0.25s | 0.4s |
| Vite (dev起動) | 0.5s | 1.5s | 3s |
| tsup | 0.3s | 0.8s | 1.5s |
アンチパターン
AP-1: tsc でトランスパイルとバンドルを兼ねる
// NG: tsc だけで全てをやろうとする
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
// 問題:
// - バンドルされない(ファイルが分散)
// - Tree-shaking なし
// - パス解決が壊れることがある(paths)
// - ビルドが遅い
// - node_modules から直接 import が必要
// OK: 型チェックとビルドを分離
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js",
"prebuild": "npm run typecheck"
}
}AP-2: 開発サーバーなしで手動リロード
// NG: 毎回手動でビルド→実行
{
"scripts": {
"dev": "tsc && node dist/index.js"
}
}
// OK: ファイル監視で自動再起動
{
"scripts": {
"dev": "tsx watch src/index.ts"
}
}
// もしくは
{
"scripts": {
"dev": "node --import @swc-node/register/esm --watch src/index.ts"
}
}AP-3: 型チェックをビルドに含めて CI を遅くする
// NG: ビルドと型チェックを直列実行
{
"scripts": {
"build": "tsc --noEmit && esbuild src/index.ts --bundle --outfile=dist/index.js"
}
}
// OK: CI で並列実行
// .github/workflows/ci.yml
// jobs:
// typecheck:
// run: npm run typecheck
// build:
// run: npm run build:js
// lint:
// run: npm run lint
// test:
// run: npm testAP-4: バンドルサイズを確認せずにデプロイ
// NG: バンドル分析なしでデプロイ
// → 不要な依存が含まれ、パフォーマンス悪化
// OK: バンドル分析を定期的に実施
// vite.config.ts
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
visualizer({
filename: "dist/stats.html",
gzipSize: true,
brotliSize: true,
}),
],
});
// npm run build 後に dist/stats.html を確認実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
トラブルシューティング
よくあるエラーと解決策
| エラー | 原因 | 解決策 |
|---|---|---|
| 初期化エラー | 設定ファイルの不備 | 設定ファイルのパスと形式を確認 |
| タイムアウト | ネットワーク遅延/リソース不足 | タイムアウト値の調整、リトライ処理の追加 |
| メモリ不足 | データ量の増大 | バッチ処理の導入、ページネーションの実装 |
| 権限エラー | アクセス権限の不足 | 実行ユーザーの権限確認、設定の見直し |
| データ不整合 | 並行処理の競合 | ロック機構の導入、トランザクション管理 |
デバッグの手順
- エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
- 再現手順の確立: 最小限のコードでエラーを再現する
- 仮説の立案: 考えられる原因をリストアップする
- 段階的な検証: ログ出力やデバッガを使って仮説を検証する
- 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
# ロガーの設定
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
def debug_decorator(func):
"""関数の入出力をログ出力するデコレータ"""
@wraps(func)
def wrapper(*args, **kwargs):
logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
try:
result = func(*args, **kwargs)
logger.debug(f"戻り値: {func.__name__} -> {result}")
return result
except Exception as e:
logger.error(f"例外発生: {func.__name__}: {e}")
logger.error(traceback.format_exc())
raise
return wrapper
@debug_decorator
def process_data(items):
"""データ処理(デバッグ対象)"""
if not items:
raise ValueError("空のデータ")
return [item * 2 for item in items]パフォーマンス問題の診断
パフォーマンス問題が発生した場合の診断手順:
- ボトルネックの特定: プロファイリングツールで計測
- メモリ使用量の確認: メモリリークの有無をチェック
- I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
- 同時接続数の確認: コネクションプールの状態を確認
| 問題の種類 | 診断ツール | 対策 |
|---|---|---|
| CPU負荷 | cProfile, py-spy | アルゴリズム改善、並列化 |
| メモリリーク | tracemalloc, objgraph | 参照の適切な解放 |
| I/Oボトルネック | strace, iostat | 非同期I/O、キャッシュ |
| DB遅延 | EXPLAIN, slow query log | インデックス、クエリ最適化 |
設計判断ガイド
選択基準マトリクス
技術選択を行う際の判断基準を以下にまとめます。
| 判断基準 | 重視する場合 | 妥協できる場合 |
|---|---|---|
| パフォーマンス | リアルタイム処理、大規模データ | 管理画面、バッチ処理 |
| 保守性 | 長期運用、チーム開発 | プロトタイプ、短期プロジェクト |
| スケーラビリティ | 成長が見込まれるサービス | 社内ツール、固定ユーザー |
| セキュリティ | 個人情報、金融データ | 公開データ、社内利用 |
| 開発速度 | MVP、市場投入スピード | 品質重視、ミッションクリティカル |
アーキテクチャパターンの選択
| アーキテクチャ選択フロー |
|---|
| ① チーム規模は? |
| ├─ 小規模(1-5人)→ モノリス |
| └─ 大規模(10人+)→ ②へ |
| ② デプロイ頻度は? |
| ├─ 週1回以下 → モノリス + モジュール分割 |
| └─ 毎日/複数回 → ③へ |
| ③ チーム間の独立性は? |
| ├─ 高い → マイクロサービス |
| └─ 中程度 → モジュラーモノリス |
トレードオフの分析
技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:
1. 短期 vs 長期のコスト
- 短期的に速い方法が長期的には技術的負債になることがある
- 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く
2. 一貫性 vs 柔軟性
- 統一された技術スタックは学習コストが低い
- 多様な技術の採用は適材適所が可能だが、運用コストが増加
3. 抽象化のレベル
- 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
- 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
"""ADR (Architecture Decision Record) の作成"""
def __init__(self, title: str):
self.title = title
self.context = ""
self.decision = ""
self.consequences = []
self.alternatives = []
def set_context(self, context: str):
"""背景と課題の記述"""
self.context = context
return self
def set_decision(self, decision: str):
"""決定内容の記述"""
self.decision = decision
return self
def add_consequence(self, consequence: str, positive: bool = True):
"""結果の追加"""
self.consequences.append({
'description': consequence,
'type': 'positive' if positive else 'negative'
})
return self
def add_alternative(self, name: str, reason_rejected: str):
"""却下した代替案の追加"""
self.alternatives.append({
'name': name,
'reason_rejected': reason_rejected
})
return self
def to_markdown(self) -> str:
"""Markdown形式で出力"""
md = f"# ADR: {self.title}\n\n"
md += f"## 背景\n{self.context}\n\n"
md += f"## 決定\n{self.decision}\n\n"
md += "## 結果\n"
for c in self.consequences:
icon = "✅" if c['type'] == 'positive' else "⚠️"
md += f"- {icon} {c['description']}\n"
md += "\n## 却下した代替案\n"
for a in self.alternatives:
md += f"- **{a['name']}**: {a['reason_rejected']}\n"
return mdFAQ
Q1: tsx と ts-node の違いは何ですか?
tsx は esbuild ベースで起動が非常に高速です。ts-node は tsc ベースで型チェックも行えますが遅いです。開発時の実行は tsx を推奨します。型チェックは別途 tsc --noEmit で行ってください。ts-node にも --swc フラグがありますが、tsx の方がセットアップが簡単で高速です。
Q2: esbuild は enum をサポートしていますか?
esbuild は const enum を通常の enum として扱います。isolatedModules: true を設定していれば問題ありません(const enum の cross-file inlining が無効化されるため)。verbatimModuleSyntax を使う場合も同様です。通常の enum は問題なくサポートされています。
Q3: Vite の本番ビルドが開発時と挙動が異なることはありますか?
はい。開発時は esbuild でトランスパイルし ESM をそのまま配信しますが、本番は Rollup でバンドルします。稀に挙動の差が出る場合があります。vite preview で本番ビルドをローカルで確認することを推奨します。具体的な差異としては、CSS の読み込み順序、動的インポートの分割粒度、環境変数の解決タイミングなどがあります。
Q4: Node.js 23+ の --experimental-strip-types は tsx の代替になりますか?
部分的には代替になります。Node.js のネイティブ TypeScript サポートは型注釈を単純に除去するだけで、enum、namespace、decorators などの TypeScript 固有の構文はサポートしません。また、tsconfig.json の paths も解決しません。シンプルな TypeScript ファイルの実行には使えますが、複雑なプロジェクトでは tsx が引き続き推奨されます。
Q5: ビルドツールの移行は困難ですか?
多くの場合、トランスパイラの移行は比較的容易です。esbuild → SWC、webpack → Vite などの移行は設定ファイルの書き換えが主な作業です。ただし、カスタムプラグインや特殊な設定に依存している場合は移行コストが高くなります。まず新ツールで小規模なプロジェクトを試してから、段階的に移行することを推奨します。
まとめ表
| 概念 | 要点 |
|---|---|
| 分離原則 | 型チェック(tsc)とトランスパイルは別ツールに |
| esbuild | Go 製、最速、バンドル可能、型チェックなし |
| SWC | Rust 製、最速、Next.js 組込み、デコレータ完全サポート |
| Vite | 開発 = esbuild、本番 = Rollup、HMR 優秀 |
| tsup | esbuild ベースのライブラリビルダー、DTS 生成 |
| tsx | esbuild ベースの ts-node 代替、高速、設定不要 |
| Turborepo | モノレポのビルドキャッシュ、並列実行 |
| unbuild | Rollup ベースのライブラリビルダー |
10. Docker でのビルド最適化
10-1. マルチステージビルド
# ---- Builder ステージ ----
FROM node:20-slim AS builder
WORKDIR /app
# 依存のインストール(キャッシュ効率化)
COPY package.json package-lock.json ./
RUN npm ci
# ソースコピー & ビルド
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
# ---- Production ステージ ----
FROM node:20-slim AS production
WORKDIR /app
# 本番依存のみインストール
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# ビルド成果物のみコピー
COPY --from=builder /app/dist ./dist
# Node.js で直接実行
CMD ["node", "dist/index.js"]10-2. esbuild でバンドルした場合
# esbuild でバンドルすると node_modules が不要になる
FROM node:20-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
RUN npx esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=esm --minify
# ---- 軽量な Production ステージ ----
FROM node:20-slim AS production
WORKDIR /app
# バンドル済みファイルのみコピー(node_modules 不要)
COPY --from=builder /app/dist/index.js ./index.js
CMD ["node", "index.js"]
# → イメージサイズが大幅に削減される10-3. .dockerignore の設定
# .dockerignore
node_modules
dist
.git
.github
*.md
.env*
.tsbuildinfo
coverage
.vscode
.idea
11. ビルドパフォーマンスのデバッグ
11-1. tsc のパフォーマンス分析
# 詳細な診断情報
tsc --extendedDiagnostics --noEmit
# 出力例:
# Files: 1,234
# Lines of Library: 35,678
# Lines of Definitions: 89,012
# Lines of TypeScript: 67,890
# Nodes: 345,678
# Identifiers: 123,456
# Symbols: 67,890
# Types: 34,567
# Instantiations: 234,567 ← これが大きいと遅い
# Memory used: 456,789K
# Assignability cache size: 12,345
# Identity cache size: 1,234
# Subtype cache size: 2,345
# Strict subtype cache: 3,456
# I/O Read time: 0.12s
# Parse time: 1.23s
# ResolveModule time: 0.34s
# ResolveTypeRef time: 0.05s
# Bind time: 0.45s
# Check time: 5.67s ← 通常最大
# printTime time: 0.89s
# Emit time: 0.89s
# Total time: 8.36s
# Instantiations が大きい場合の対処:
# 1. 複雑なジェネリクス型を簡素化
# 2. 条件型のネストを減らす
# 3. 型の再計算を避ける(type alias でキャッシュ)11-2. 型のパフォーマンスを改善するテクニック
// NG: 深くネストされた条件型(Instantiations が爆発)
type DeepPick<T, K extends string> =
K extends `${infer First}.${infer Rest}`
? First extends keyof T
? { [P in First]: DeepPick<T[First], Rest> }
: never
: K extends keyof T
? { [P in K]: T[P] }
: never;
// OK: interface で中間型を定義してキャッシュ
interface CachedDeepPick<T, First extends keyof T, Rest extends string> {
[P in First]: DeepPick<T[First], Rest>;
}
// NG: 大量のユニオン型(チェックが O(n^2))
type AllEvents = Event1 | Event2 | ... | Event100;
// OK: 判別共用体でマップ型を使用
interface EventMap {
event1: Event1;
event2: Event2;
// ...
}
type AllEvents = EventMap[keyof EventMap];まとめ
このガイドでは以下の重要なポイントを学びました:
- 基本概念と原則の理解
- 実践的な実装パターン
- ベストプラクティスと注意点
- 実務での活用方法
次に読むべきガイド
- tsconfig.json -- ビルドツールと連携する TypeScript 設定
- テスト -- Vitest の設定とビルドツールの連携
- ESLint + TypeScript -- ビルドパイプラインへの lint 統合
参考文献
-
esbuild -- An extremely fast bundler for the web https://esbuild.github.io/
-
SWC -- Rust-based platform for the Web https://swc.rs/
-
Vite -- Next Generation Frontend Tooling https://vitejs.dev/
-
tsx -- TypeScript Execute https://tsx.is/
-
tsup -- Bundle your TypeScript library with no config https://tsup.egoist.dev/
-
Turborepo -- High-performance build system for JavaScript and TypeScript codebases https://turbo.build/repo
-
unbuild -- A unified JavaScript build system https://github.com/unjs/unbuild