プロジェクトを速く・堅くするインフラ
型が正しくても、ツールの設定が間違っていれば意味がない。
この記事で得られること
- tsconfig の全オプションの意味と最適設定を理解する
- esbuild・SWC・tscをいつ使い分けるかを判断できる
- 型定義ファイル(.d.ts)を自分で書けるようになる
- 型レベルのテストを導入してライブラリ品質の型安全性を担保できる
Chapter 1: tsconfig 完全解説 2025
1-1. tsconfig の基本構造
{
"compilerOptions": { /* コンパイラの動作設定 */ },
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"],
"references": [] /* Project References(後述) */
}
1-2. 型チェック系オプション(最重要)
{
"compilerOptions": {
// ✅ strict: true を必ず有効に(以下の全てを一括で有効化)
"strict": true,
// strict が有効にするオプション(個別制御も可能):
// "strictNullChecks": true → null/undefined を明示的に扱う
// "strictFunctionTypes": true → 関数の引数を反変チェック
// "strictBindCallApply": true → bind/call/apply を型安全に
// "strictPropertyInitialization": true → クラスプロパティの初期化を強制
// "noImplicitAny": true → 暗黙的な any を禁止
// "noImplicitThis": true → this の型が不明な場合にエラー
// "alwaysStrict": true → "use strict" を全ファイルに付与
// 追加で推奨するオプション
"noUncheckedIndexedAccess": true, // arr[i] の型に undefined を含める
"noImplicitOverride": true, // override キーワードを強制
"exactOptionalPropertyTypes": true, // undefined の明示的な代入を禁止
"noPropertyAccessFromIndexSignature": true // インデックス型へのドットアクセスを禁止
}
}
1-3. モジュール系オプション
{
"compilerOptions": {
// 2025年のおすすめ設定
"module": "NodeNext", // または "Bundler"(Next.js, Vite等を使う場合)
"moduleResolution": "NodeNext", // "module" に合わせる
// verbatimModuleSyntax(TS 5.0+):type-only import を強制
// import type { Foo } from './foo' と書かなければならない
"verbatimModuleSyntax": true,
// paths エイリアス(monorepo や絶対パス import に便利)
"paths": {
"@/*": ["./src/*"]
},
// baseUrl と paths を一緒に使う場合
"baseUrl": "."
}
}
1-4. 出力系オプション
{
"compilerOptions": {
// トランスパイルはesbuild/SWCに任せ、型チェックのみ行う場合
"noEmit": true,
// 型定義ファイルを生成する場合(ライブラリ開発)
"declaration": true,
"declarationMap": true, // .d.ts のソースマップ
"declarationDir": "dist/types",
// インクリメンタルコンパイル(再ビルド高速化)
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
// ターゲット環境
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
// source map(デバッグ用)
"sourceMap": true,
"inlineSources": true
}
}
1-5. Project References:大規模プロジェクトの高速化
モノレポや大きなプロジェクトでは、Project Referencesを使ってコンパイルを分割します。
monorepo/
├── tsconfig.json # ルート(参照のみ)
└── packages/
├── core/
│ ├── tsconfig.json # composite: true
│ └── src/
├── ui/
│ ├── tsconfig.json # composite: true, references: [core]
│ └── src/
└── app/
├── tsconfig.json # composite: true, references: [ui, core]
└── src/
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true, // Project References に必須
"declaration": true, // composite では必須
"outDir": "dist",
"rootDir": "src"
}
}
// packages/app/tsconfig.json
{
"compilerOptions": { "composite": true },
"references": [
{ "path": "../core" },
{ "path": "../ui" }
]
}
tsc --build コマンドで変更があったパッケージのみ再コンパイルされます。
1-6. 環境別 tsconfig の分割
// tsconfig.base.json(共通設定)
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"moduleResolution": "NodeNext"
}
}
// tsconfig.json(型チェック用)
{
"extends": "./tsconfig.base.json",
"compilerOptions": { "noEmit": true },
"include": ["src", "tests"]
}
// tsconfig.build.json(ビルド用)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src"],
"exclude": ["**/*.test.ts"]
}
Chapter 2: esbuild・SWC・tscの使い分け
2-1. 3つのツールの役割の違い
| ツール | 役割 | 型チェック | 速度 |
|---|---|---|---|
tsc | 型チェック + トランスパイル | ✅ 完全 | 遅い(AST解析が重い) |
esbuild | トランスパイルのみ | ❌ なし | 非常に速い(Go製) |
SWC | トランスパイルのみ | ❌ なし | 非常に速い(Rust製) |
2-2. tsc の使いどころ
# 型チェックのみ(CI/CD で使う)
tsc --noEmit
# 型チェック + 出力(ライブラリ開発・型定義ファイルの生成)
tsc --build
# ウォッチモード
tsc --watch --noEmit
tsc はトランスパイルが遅いため、アプリケーション開発では「型チェック専用」として使い、トランスパイルは esbuild や SWC に任せるのが現代のベストプラクティスです。
2-3. esbuild の使いどころ
// esbuild は型チェックをしない高速トランスパイラ
import * as esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/index.js",
platform: "node",
target: "node20",
format: "esm",
});
採用ケース:
- Vite(内部でesbuildを使用)
- バンドルサイズや速度を最優先にしたい場合
- Node.js の CLIツール開発
2-4. SWC の使いどころ
// .swcrc
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": true
},
"transform": {
"react": { "runtime": "automatic" }
},
"target": "es2022"
}
}
採用ケース:
- Next.js(デフォルトでSWCを使用)
- NestJS(
@swc/cliの利用) - Jest のトランスフォーム(
@swc/jest)
2-5. 実務での推奨構成
// package.json
{
"scripts": {
// 型チェックは tsc(CIと開発中の確認に使う)
"typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch",
// ビルドは esbuild(速い)
"build": "esbuild src/index.ts --bundle --outfile=dist/index.js",
// テストは vitest(SWCでトランスパイル)
"test": "vitest",
// 型定義ファイルの生成は tsc
"build:types": "tsc --project tsconfig.build.json"
}
}
CI での理想的なパイプライン:
tsc --noEmit(型チェック)eslint(静的解析)vitest(テスト、SWCでトランスパイル)esbuild(本番ビルド)
Chapter 3: 型定義ファイル(.d.ts)の書き方
3-1. .d.ts ファイルとは何か
型定義ファイルは「このJSファイルにはこういう型がある」とTypeScriptに伝えるためのファイルです。
node_modules/
└── some-library/
├── index.js ← 実際のJavaScriptコード
└── index.d.ts ← 型情報(TypeScriptが読む)
3-2. 基本的な .d.ts の書き方
// types/my-library.d.ts
// 関数の型定義
export declare function greet(name: string): string;
export declare function add(a: number, b: number): number;
// 変数・定数の型定義
export declare const VERSION: string;
export declare let config: Config;
// インターフェースと型
export declare interface Config {
timeout: number;
retries: number;
baseUrl: string;
}
export declare type Status = "active" | "inactive" | "pending";
// クラスの型定義
export declare class ApiClient {
constructor(config: Config);
get<T>(url: string): Promise<T>;
post<T>(url: string, body: unknown): Promise<T>;
}
// デフォルトエクスポート
declare const defaultExport: ApiClient;
export default defaultExport;
3-3. グローバル型定義
// types/global.d.ts
// グローバル変数の型定義
declare const __APP_VERSION__: string;
declare const __DEV__: boolean;
// グローバルオブジェクトの拡張
declare global {
interface Window {
analytics: Analytics;
dataLayer: object[];
}
interface Array<T> {
// 独自メソッドを追加(注意:既存メソッドを上書きしない)
groupBy<K extends string>(keyFn: (item: T) => K): Record<K, T[]>;
}
}
export {}; // これがないと global.d.ts としてみなされない
3-4. モジュール拡張(Declaration Merging)
// 既存ライブラリの型を拡張する
// types/express.d.ts
import "express";
declare module "express" {
interface Request {
user?: AuthenticatedUser;
requestId: string;
}
}
// 使用例(自動補完が効く)
app.use((req, res, next) => {
req.user; // AuthenticatedUser | undefined
req.requestId; // string
next();
});
3-5. ライブラリ向け .d.ts の自動生成
// tsconfig.build.json
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"declarationDir": "dist/types",
"emitDeclarationOnly": true
}
}
# 型定義ファイルのみを生成
tsc --project tsconfig.build.json
// package.json(ライブラリの公開設定)
{
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/types/index.d.ts", // 型定義ファイルを指定
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/types/index.d.ts"
}
}
}
Chapter 4: 型テスト(type-level testing)
4-1. なぜ型テストが必要か
// この型ユーティリティは正しく動いているか?
type Flatten<T> = T extends Array<infer U> ? U : T;
// 手動で確認するのは限界がある
type Test1 = Flatten<string[]>; // string のはず
type Test2 = Flatten<number>; // number のはず
type Test3 = Flatten<string[][]>; // string[] のはず? string のはず?
型ユーティリティが「正しい型を返すか」を自動テストできれば、リファクタリング時の安全性が格段に上がります。
4-2. expectType ヘルパーを自作する
最もシンプルな方法は、型レベルのアサーション関数を作ることです。
// test/type-utils.ts
// T と Expected が完全に一致することをチェック
type Expect<T extends true> = T;
type Equal<A, B> = A extends B ? B extends A ? true : false : false;
// 使用例
type _tests = [
Expect<Equal<Flatten<string[]>, string>>,
Expect<Equal<Flatten<number>, number>>,
Expect<Equal<Flatten<string[][]>, string[]>>,
// 間違えると: Type 'false' does not satisfy the constraint 'true'
];
4-3. tsd を使う
tsd はCLIで型テストを実行できるツールです。
npm install -D tsd
// index.test-d.ts
import { expectType, expectError, expectAssignable } from "tsd";
import { Flatten, UserId } from "./index";
// 型が期待通りか確認
expectType<string>(null as unknown as Flatten<string[]>);
expectType<number>(null as unknown as Flatten<number>);
// エラーになることを確認
expectError(UserId("invalid_format")); // バリデーションエラーを期待
// 代入可能性を確認
expectAssignable<string>(null as unknown as UserId);
// package.json
{
"scripts": {
"test:types": "tsd"
}
}
4-4. vitest-axe と @vitest/expect の型テスト
Vitestを使っている場合は、expectTypeOf が組み込みで使えます。
// tests/types.test.ts
import { expectTypeOf, test } from "vitest";
import type { Flatten, Result } from "../src/types";
test("Flatten removes one layer of array", () => {
expectTypeOf<Flatten<string[]>>().toEqualTypeOf<string>();
expectTypeOf<Flatten<number>>().toEqualTypeOf<number>();
expectTypeOf<Flatten<string[][]>>().toEqualTypeOf<string[]>();
});
test("Result ok has value property", () => {
type OkResult = Extract<Result<User>, { ok: true }>;
expectTypeOf<OkResult>().toHaveProperty("value");
expectTypeOf<OkResult["value"]>().toEqualTypeOf<User>();
});
test("Result error has error property", () => {
type ErrResult = Extract<Result<User, FetchError>, { ok: false }>;
expectTypeOf<ErrResult["error"]>().toEqualTypeOf<FetchError>();
});
4-5. 型テストの CI 統合
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20" }
- run: npm ci
# 型チェック
- run: npm run typecheck
# 型テスト
- run: npm run test:types
# 通常のテスト(型テストを含む)
- run: npm test
4-6. 型テストのベストプラクティス
// ✅ 境界値を必ずテストする
type _BoundaryTests = [
// never が返ることを確認
Expect<Equal<KeysOfType<{}, string>, never>>,
// Union型が正しく処理されることを確認
Expect<Equal<Flatten<string[] | number[]>, string | number>>,
// any が漏れていないことを確認
Expect<Equal<IsAny<string>, false>>,
Expect<Equal<IsAny<any>, true>>,
];
// ✅ エラーになるべきケースも書く
// @ts-expect-error ← このコメントで「エラーが出ることを期待」
const invalid: UserId = "user_123"; // Brand なしの string は代入不可
まとめ:Vol.4 のチェックリスト
- [ ]
strict: trueとそれが有効にするオプションを全て説明できる - [ ]
noEmitとemitDeclarationOnlyの使い分けを理解している - [ ] Project References を設定してモノレポのビルドを最適化できる
- [ ] tsc・esbuild・SWC をいつ何に使うか説明できる
- [ ]
.d.tsファイルを手書きできる - [ ] Declaration Merging で既存ライブラリの型を拡張できる
- [ ]
expectTypeOfまたはtsdで型テストを書ける - [ ] 型テストを CI パイプラインに組み込める
シリーズ全体のまとめ
| Vol | テーマ | 核心 |
|---|---|---|
| Vol.1 | 基礎・思想 | 型を「なぜ書くか」を理解する |
| Vol.2 | 型システム深掘り | 型を「計算・変換」できる |
| Vol.3 | 実践設計パターン | 型で「バグを不可能」にする |
| Vol.4 | ツール・エコシステム | 型を「プロジェクト全体」に活かす |
4本を通して読んだあなたは、TypeScriptを「型のあるJavaScript」としてではなく、「型による設計言語」として使いこなせるはずです。
シリーズを最後まで読んでいただきありがとうございます。 質問・フィードバックはコメントにてどうぞ。