こんにちは、株式会社kubellでフロントエンドエンジニアをしている石田(@ishida_2002)です。
この記事は kubell Advent Calendar 2025 の記事です🎄
私の所属しているフロントエンドチームでは Renovate を利用して依存関係の更新を行なっています。
今回は、Renovate が発行した PR をできるだけノールックマージするために、弊チームが取り組んでいる内容を紹介したいと思います。
はじめに
@types/ パッケージを使用していると、本体パッケージとのバージョン不整合という問題に遭遇することがあります。
例えば、以下のような状況です。
{ "dependencies": { "react": "18.0.0" }, "devDependencies": { "@types/react": "19.0.0" } }
react は v18 ですが、@types/react は v19 の型定義を参照しています。この場合に v19 で追加された API を利用すると、型定義は存在するためビルドは通るものの、実行時には存在しない API を呼び出してエラーになってしまいます。
このような事態を避けるために @types/ パッケージの更新時は本体バージョンを確認していましたが、地味に面倒ですし、たまに忘れてマージしてしまうこともありました。
というわけで、自動化しましょう🤖
概要
以下の処理を行うスクリプトを作成します。
package.jsonから@types/パッケージを抽出してバージョンを取得- 対応する本体パッケージのバージョンを取得
- バージョンを比較し、
@types/が本体より新しい場合にエラーを返す
なお、パッチバージョン(x.y.z の z)は比較対象に含めません。これは @types/ パッケージが独自のパッチリリースを行うことがあるためです。
実現方法
パッケージ名の変換
@types/ パッケージ名から本体パッケージ名への変換を行います。
スコープ付きパッケージ(@scope/package)の場合、DefinitelyTyped の命名規則では @types/scope__package となるため、__ を / に戻す処理が必要です。
function convertPackageName(originalPackageName: string): string { if (originalPackageName.includes('__')) { return originalPackageName.replace('@types/', '@').replace('__', '/'); } return originalPackageName.replace('@types/', ''); } convertPackageName('@types/react') // 'react' convertPackageName('@types/babel__core') // '@babel/core'
バージョンのフォーマット
セマンティックバージョニング(x.y.z)を major.minor(x.y)形式に変換します。
const regex = new RegExp(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/); function formatVersion(version: string): string | undefined { if (!regex.test(version)) return undefined; const arr = version.match(regex); if (!arr) return undefined; return `${arr[1]}.${arr[2]}`; } formatVersion('1.2.3') // '1.2'
バージョンを比較
compare-versions というライブラリを利用してバージョンを比較します。
import { compareVersions } from 'compare-versions'; function canUpdateTypesPackageToNewerVersion( realVersion?: string, typesVersion?: string, ): boolean { if (!(realVersion && typesVersion)) return true; return compareVersions(realVersion, typesVersion) >= 0; }
@types パッケージの抽出
devDependencies から @types/ で始まるパッケージを抽出して Map として返します。
function packageJsonToTypesDependenciesMap( packageJson: { devDependencies: Record<string, string> }, ): Map<string, string | undefined> { const { devDependencies } = packageJson; const devDependencyWithVersions: Map<string, string | undefined> = Object.entries( devDependencies, ) .filter(([name]) => name.startsWith('@types/')) // @types パッケージを抽出 .map(([name, version]) => [convertPackageName(name), version]) .reduce( (map, [name, version]) => map.set(name, formatVersion(version)), new Map<string, string | undefined>(), ); return devDependencyWithVersions; }
対応する本体パッケージのバージョンを取得
@types/ パッケージに対応する本体パッケージのバージョンを取得します。
function packageJsonToDependenciesMap( packageJson: { dependencies: Record<string, string> }, typesPackagesMap: Map<string, string | undefined>, ): Map<string, string | undefined> { const { dependencies } = packageJson; const dependencyWithVersions = new Map<string, string | undefined>(); typesPackagesMap.forEach((_, name) => { const version = dependencies[name] ?? ''; dependencyWithVersions.set(name, formatVersion(version)); }); return dependencyWithVersions; }
組み合わせる
まとめるとこんな感じです
import { readFile } from 'node:fs/promises'; // 略 async function main(): Promise<void> { const newPackageJsonPath = process.argv[2]; const file = await readFile(newPackageJsonPath, 'utf-8'); const newPackageJson = JSON.parse(file) as { devDependencies: Record<string, string>; dependencies: Record<string, string>; }; // 1. @types パッケージを取得 const typesDevDependencies = packageJsonToTypesDependenciesMap( newPackageJson, ); // 2. 対応する本体パッケージを取得 const correspondDependencies = packageJsonToDependenciesMap( newPackageJson, typesDevDependencies, ); // 3. バージョンを比較し、`@types/` が本体より新しい場合にエラーを返す typesDevDependencies.forEach((typesVersion, name) => { const realVersion = correspondDependencies.get(name); if (!canUpdateTypesPackageToNewerVersion(realVersion, typesVersion)) { console.error('@types/ パッケージが本体のバージョンを上回っています'); process.exit(1); } }); console.log('OK 🎉'); }
CI/CD への組み込み
package.json に差分がある場合に発火するようにすれば OK です
# GitHub Actions name: Check Types Version on: pull_request: paths: - 'package.json' jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '24' - run: npm ci - run: npx tsx check-types-version.ts <package.json path>
終わりに
これで @types/ パッケージは CI が通っていたらノールックでマージできます🎉
ぜひあなたのプロジェクトにも導入してみてください!
余談ですが、弊チームでは似たような理由から ESLint も自動テストしています。詳しくは別の記事があるのでぜひご覧ください。
明日は25新卒の @10yaPrestage さんです🎄