T-CREATOR

TypeScript 破壊的変更をブロック!CI で型の後方互換性をチェックする仕組み

TypeScript 破壊的変更をブロック!CI で型の後方互換性をチェックする仕組み

TypeScript でライブラリやパッケージを開発していると、型定義の変更が利用者に与える影響を見落としがちです。「ちょっと型を修正しただけ」のつもりが、実は利用者側のコードをすべて壊してしまう破壊的変更になっていた、という経験はありませんか。

本記事では、CI パイプラインで型の後方互換性を自動チェックし、破壊的変更を未然に防ぐ仕組みについて解説します。実際に使える具体的なツールと実装方法も紹介しますので、ぜひ最後までお読みください。

背景

TypeScript におけるライブラリ開発の課題

TypeScript を使ったライブラリ開発では、JavaScript の実行時動作だけでなく、型定義の互換性 も重要な要素となります。ライブラリのメジャーバージョンを上げずにマイナーバージョンやパッチバージョンを更新する場合、既存の利用者が影響を受けないようにする必要があります。

しかし、型システムは複雑で、一見すると問題なさそうな変更でも、利用者のコードで型エラーを引き起こすことがあるのです。

セマンティックバージョニングと型の関係

npm パッケージは一般的に セマンティックバージョニング(SemVer) に従います。バージョン番号は major.minor.patch の形式で表され、それぞれ以下の意味を持ちます。

#バージョン変更内容後方互換性
1Major破壊的変更を含むなし
2Minor新機能追加あり
3Patchバグ修正あり

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.jsonexports フィールドの検証
  • 型定義ファイルの存在確認
  • 軽量で高速

インストール方法

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.enabledtrue にすることで、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

ビルド後、attwapi-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:パッケージングの問題を検出

実装のステップ

  1. プロジェクトに必要なツールをインストール
  2. API レポートを初回生成してコミット
  3. GitHub Actions で自動チェックを設定
  4. プルリクエストで破壊的変更を検出

これらの仕組みを導入することで、開発チームは安心してライブラリを進化させることができます。型の破壊的変更を CI で自動検出する仕組みは、今や TypeScript ライブラリ開発のベストプラクティスとなっています。

ぜひ、皆さんのプロジェクトにも導入してみてください。最初は設定に少し時間がかかりますが、一度導入すれば長期的に大きなメリットを得られます。

関連リンク