TypeScript 破壊的変更をブロック!CI で型の後方互換性をチェックする仕組み
TypeScript でライブラリやパッケージを開発していると、型定義の変更が利用者に与える影響を見落としがちです。「ちょっと型を修正しただけ」のつもりが、実は利用者側のコードをすべて壊してしまう破壊的変更になっていた、という経験はありませんか。
本記事では、CI パイプラインで型の後方互換性を自動チェックし、破壊的変更を未然に防ぐ仕組みについて解説します。実際に使える具体的なツールと実装方法も紹介しますので、ぜひ最後までお読みください。
背景
TypeScript におけるライブラリ開発の課題
TypeScript を使ったライブラリ開発では、JavaScript の実行時動作だけでなく、型定義の互換性 も重要な要素となります。ライブラリのメジャーバージョンを上げずにマイナーバージョンやパッチバージョンを更新する場合、既存の利用者が影響を受けないようにする必要があります。
しかし、型システムは複雑で、一見すると問題なさそうな変更でも、利用者のコードで型エラーを引き起こすことがあるのです。
セマンティックバージョニングと型の関係
npm パッケージは一般的に セマンティックバージョニング(SemVer) に従います。バージョン番号は major.minor.patch の形式で表され、それぞれ以下の意味を持ちます。
| # | バージョン | 変更内容 | 後方互換性 |
|---|---|---|---|
| 1 | Major | 破壊的変更を含む | なし |
| 2 | Minor | 新機能追加 | あり |
| 3 | Patch | バグ修正 | あり |
TypeScript の型定義も、このルールに従う必要があります。Minor や Patch バージョンアップで型の破壊的変更を含めてしまうと、利用者のビルドが突然失敗する事態を招きます。
以下の図は、バージョン管理と型互換性の関係を示したものです。
mermaidflowchart TB
ver["バージョンアップ"] --> check{"破壊的変更<br/>あり?"}
check -->|Yes| major["Major バージョン<br/>例: 1.0.0 → 2.0.0"]
check -->|No| minor_patch{"新機能<br/>あり?"}
minor_patch -->|Yes| minor["Minor バージョン<br/>例: 1.0.0 → 1.1.0"]
minor_patch -->|No| patch["Patch バージョン<br/>例: 1.0.0 → 1.0.1"]
major --> allowed_major["型の破壊的変更 OK"]
minor --> not_allowed["型の破壊的変更 NG"]
patch --> not_allowed
このように、Minor や Patch では型の破壊的変更は許されません。しかし、開発者が手動でこれを確認するのは困難です。
型定義の進化とメンテナンス
TypeScript のライブラリは時間とともに進化します。新しい機能を追加し、既存の API を改善し、パフォーマンスを最適化していきます。この過程で型定義も変更されますが、その変更が既存の利用者にどのような影響を与えるかを常に意識する必要があります。
人間の目視チェックだけでは、以下のような問題が発生しやすくなります。
- 型パラメータの変更に気づかない
- オプショナルプロパティを必須に変えてしまう
- 戻り値の型を狭めてしまう
- ジェネリクスの制約を厳しくしてしまう
これらを防ぐには、自動化されたチェック が不可欠です。
課題
破壊的変更とは何か
TypeScript における破壊的変更とは、既存の利用者のコードが型エラーになる変更 を指します。具体的には以下のようなケースが該当します。
| # | 変更内容 | 影響 |
|---|---|---|
| 1 | 必須プロパティの追加 | 利用者がオブジェクトを作成できなくなる |
| 2 | プロパティの削除 | 利用者がアクセスできなくなる |
| 3 | 型の厳格化 | 利用者の引数が型エラーになる |
| 4 | 戻り値の型の変更 | 利用者の型推論が壊れる |
| 5 | ジェネリクスの制約変更 | 利用者の型パラメータが不適合になる |
破壊的変更の具体例
実際にどのような変更が破壊的になるのか、コード例で見てみましょう。
例 1:オプショナルプロパティを必須に変更
変更前(v1.0.0)
typescript// ライブラリ側の型定義
interface UserConfig {
name: string;
age?: number; // オプショナル
}
変更後(v1.0.1)
typescript// ライブラリ側の型定義
interface UserConfig {
name: string;
age: number; // 必須に変更
}
利用者側への影響
typescript// 利用者のコード(v1.0.0 では動作していた)
const config: UserConfig = {
name: '太郎',
// age を省略
};
// v1.0.1 にアップデートすると型エラー発生
// Error: Property 'age' is missing in type '{ name: string; }'
この変更は Patch バージョンアップであるにもかかわらず、破壊的変更となっています。
例 2:関数の引数型を狭める
変更前(v1.0.0)
typescript// ライブラリ側の関数
function processData(data: string | number): void {
// 処理
}
変更後(v1.1.0)
typescript// ライブラリ側の関数
function processData(data: string): void {
// number を受け付けなくなった
}
利用者側への影響
typescript// 利用者のコード(v1.0.0 では動作していた)
processData(123);
// v1.1.0 にアップデートすると型エラー発生
// Error: Argument of type 'number' is not assignable to parameter of type 'string'
Minor バージョンアップでも、このような変更は破壊的変更です。
なぜ自動チェックが必要なのか
以下の図は、手動チェックと自動チェックの違いを示しています。
mermaidflowchart LR
subgraph manual["手動チェック"]
dev1["開発者"] -->|目視確認| review1["レビュー"]
review1 -->|見落とし| release1["リリース"]
release1 -->|破壊的変更| user_error1["利用者で<br/>エラー発生"]
end
subgraph auto["自動チェック(CI)"]
dev2["開発者"] -->|コミット| ci["CI パイプライン"]
ci -->|型チェック| result{"互換性<br/>あり?"}
result -->|No| block["マージブロック"]
result -->|Yes| release2["安全に<br/>リリース"]
end
手動チェックでは、以下の課題があります。
- レビュアーの知識や経験に依存する
- 複雑な型定義の変更を見落としやすい
- チーム全体で一貫性を保つのが難しい
- レビューに時間がかかる
一方、CI による自動チェックでは、これらの課題を解決できます。
解決策
CI で型の後方互換性をチェックする仕組み
型の後方互換性を自動的にチェックするには、以下のアプローチが有効です。
| # | アプローチ | 概要 |
|---|---|---|
| 1 | 型定義の比較 | 以前のバージョンと現在の型定義を比較する |
| 2 | 公開 API の抽出 | 公開している型を明示的に管理する |
| 3 | パッケージング検証 | npm パッケージとして正しく公開できるか検証する |
これらを実現するツールとして、以下の 3 つが主に使われています。
主要なツールの紹介
ツール 1:@arethetypeswrong/cli
@arethetypeswrong/cli(略称:attw)は、TypeScript パッケージの型定義が正しくエクスポートされているかをチェックするツールです。
特徴
- npm パッケージの型定義の問題を検出
- CommonJS と ESM の両方に対応
- CI に組み込みやすい
インストール方法
bashyarn add -D @arethetypeswrong/cli
基本的な使い方
bash# パッケージをビルドしてから実行
attw --pack .
このコマンドは、パッケージをビルドして型定義の問題を検出します。
ツール 2:@microsoft/api-extractor
Microsoft が提供する @microsoft/api-extractor は、TypeScript プロジェクトの公開 API を抽出し、API レポートを生成するツールです。
特徴
- 公開 API の定義を
.api.mdファイルとして出力 - 以前のバージョンとの diff を検出
- 破壊的変更を自動的に検出
インストール方法
bashyarn add -D @microsoft/api-extractor
設定ファイルの作成
bash# 設定ファイルを初期化
api-extractor init
ツール 3:publint
publint は、npm パッケージの package.json と型定義の整合性をチェックするツールです。
特徴
package.jsonのexportsフィールドの検証- 型定義ファイルの存在確認
- 軽量で高速
インストール方法
bashyarn add -D publint
基本的な使い方
bashpublint
CI パイプラインへの組み込み
以下の図は、CI パイプラインでの型チェックフローを示しています。
mermaidflowchart TB
start["プルリクエスト<br/>作成"] --> install["依存関係<br/>インストール"]
install --> build["TypeScript<br/>ビルド"]
build --> test["ユニット<br/>テスト"]
test --> typecheck["型互換性<br/>チェック"]
typecheck --> attw["attw 実行"]
typecheck --> api["api-extractor<br/>実行"]
typecheck --> pub["publint 実行"]
attw --> judge{"すべて<br/>パス?"}
api --> judge
pub --> judge
judge -->|Yes| merge["マージ可能"]
judge -->|No| fail["マージブロック<br/>修正が必要"]
このフローにより、破壊的変更を含むコードがマージされることを防げます。
具体例
実際のプロジェクトでの実装
実際に型の後方互換性チェックを CI に組み込む手順を、ステップバイステップで解説します。
ステップ 1:プロジェクトのセットアップ
まず、TypeScript プロジェクトの基本構成を確認します。
package.json の確認
json{
"name": "my-library",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
}
}
exports フィールドで型定義のパスを明示的に指定します。
ステップ 2:@arethetypeswrong/cli の導入
パッケージのインストール
bashyarn add -D @arethetypeswrong/cli
package.json にスクリプトを追加
json{
"scripts": {
"build": "tsc",
"test": "jest",
"typecheck": "attw --pack ."
}
}
実行してみる
bashyarn build
yarn typecheck
エラーがなければ、以下のような出力が表示されます。
textmy-library v1.0.0
Build tools:
- typescript@5.3.3
Entrypoints:
<root> 👍
No problems found
ステップ 3:@microsoft/api-extractor の導入
パッケージのインストール
bashyarn add -D @microsoft/api-extractor
設定ファイルの初期化
bashnpx api-extractor init
api-extractor.json が生成されます。
api-extractor.json の編集
json{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/dist/index.d.ts",
"apiReport": {
"enabled": true,
"reportFolder": "<projectFolder>/etc/"
},
"dtsRollup": {
"enabled": false
}
}
apiReport.enabled を true にすることで、API レポートが生成されます。
package.json にスクリプトを追加
json{
"scripts": {
"build": "tsc",
"api-report": "api-extractor run --local",
"api-check": "api-extractor run"
}
}
--local オプションを付けると、ローカルでレポートを更新します。
ステップ 4:初回の API レポート生成
ビルドして API レポートを生成
bashyarn build
yarn api-report
etc/my-library.api.md が生成されます。このファイルは Git にコミットします。
生成された API レポートの例
markdown# API Report File for "my-library"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public
export interface UserConfig {
name: string;
age?: number;
}
// @public
export function createUser(config: UserConfig): User;
```
このファイルが、今後の変更の基準となります。
ステップ 5:GitHub Actions での自動チェック
.github/workflows/typecheck.yml を作成
yamlname: Type Compatibility Check
on:
pull_request:
branches:
- main
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
まず、コードをチェックアウトします。
依存関係のインストールとキャッシュ
yaml- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
Node.js をセットアップし、依存関係をインストールします。--frozen-lockfile オプションで、yarn.lock を変更せずにインストールします。
ビルドと型チェック
yaml- name: Build
run: yarn build
- name: Check types with attw
run: yarn typecheck
- name: Check API compatibility
run: yarn api-check
ビルド後、attw と api-extractor を実行します。
ステップ 6:破壊的変更を検出するテスト
実際に破壊的変更を加えて、CI で検出されるか確認してみましょう。
型定義を変更(破壊的変更)
typescript// src/index.ts
export interface UserConfig {
name: string;
age: number; // オプショナルから必須に変更
}
export function createUser(config: UserConfig): User {
// 実装
}
コミットしてプルリクエストを作成
bashgit add .
git commit -m "feat: make age required"
git push origin feature/breaking-change
CI での実行結果
GitHub Actions が実行され、api-extractor が以下のようなエラーを出力します。
textError: API signature has changed. Please review the API report.
The API report has changed. You must review the changes in etc/my-library.api.md
and commit the updated file.
API レポートの diff を確認すると、以下のような変更が検出されます。
diff export interface UserConfig {
name: string;
- age?: number;
+ age: number;
}
このように、CI で破壊的変更が自動的に検出されます。
ステップ 7:publint の追加
さらに publint も追加して、パッケージングの問題も検出しましょう。
パッケージのインストール
bashyarn add -D publint
package.json にスクリプトを追加
json{
"scripts": {
"lint:publint": "publint"
}
}
GitHub Actions に追加
yaml- name: Check package with publint
run: yarn lint:publint
これで、package.json の設定ミスも検出できるようになります。
エラーハンドリングと通知
CI でエラーが検出された場合、開発者に適切に通知することが重要です。
GitHub Actions のステータスチェック
yaml- name: Comment PR
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '⚠️ 型の後方互換性チェックに失敗しました。API レポートを確認してください。'
})
失敗時にプルリクエストにコメントを追加します。
実際の運用フロー
以下の図は、実際の開発フローを示しています。
mermaidsequenceDiagram
participant Dev as 開発者
participant Git as Git
participant CI as CI/CD
participant Rev as レビュアー
Dev->>Git: コミット&プッシュ
Git->>CI: PR 作成トリガー
CI->>CI: ビルド実行
CI->>CI: 型チェック実行
alt 型互換性あり
CI->>Git: ✅ チェック成功
Git->>Rev: レビュー依頼
Rev->>Git: 承認
Git->>Git: マージ
else 型互換性なし
CI->>Git: ❌ チェック失敗
CI->>Dev: 通知
Dev->>Dev: 修正
Dev->>Git: 再コミット
end
このフローにより、破壊的変更が本番環境に到達する前に検出できます。
まとめ
TypeScript ライブラリの開発において、型の後方互換性は利用者の信頼を保つために非常に重要です。本記事では、CI パイプラインで型の破壊的変更を自動検出する仕組みについて解説しました。
重要なポイントをまとめます。
型の後方互換性チェックが必要な理由
- セマンティックバージョニングの遵守
- 利用者のコードを壊さない
- ライブラリの信頼性向上
主要なツール
@arethetypeswrong/cli:型定義のエクスポートをチェック@microsoft/api-extractor:公開 API の変更を検出publint:パッケージングの問題を検出
実装のステップ
- プロジェクトに必要なツールをインストール
- API レポートを初回生成してコミット
- GitHub Actions で自動チェックを設定
- プルリクエストで破壊的変更を検出
これらの仕組みを導入することで、開発チームは安心してライブラリを進化させることができます。型の破壊的変更を CI で自動検出する仕組みは、今や TypeScript ライブラリ開発のベストプラクティスとなっています。
ぜひ、皆さんのプロジェクトにも導入してみてください。最初は設定に少し時間がかかりますが、一度導入すれば長期的に大きなメリットを得られます。
関連リンク
articleTypeScript 破壊的変更をブロック!CI で型の後方互換性をチェックする仕組み
articleTypeScript SDK 設計の定石:ビルダー × ジェネリクスで直感的かつ安全な API を作る
articleTypeScript タプル/配列操作チートシート:`Length`・`Push`・`Zip`・`Chunk`を型で書く
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleTypeScript Null 安全戦略の比較検証:ts-reset vs strictNullChecks vs noUncheckedIndexedAccess
articleESM/CJS 地獄から脱出!「ERR_REQUIRE_ESM」「import 文が使えない」を TypeScript で直す
articleWebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応
articleStorybook 品質ゲート運用:Lighthouse/A11y/ビジュアル差分を PR で自動承認
articleWebRTC で高精細 1080p/4K 画面共有:contentHint「detail」と DPI 最適化
articleSolidJS フォーム設計の最適解:コントロール vs アンコントロールドの棲み分け
articleWebLLM 使い方入門:チャット UI を 100 行で実装するハンズオン
articleShell Script と Ansible/Make/Taskfile の比較:小規模自動化の最適解を検証
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来