メインコンテンツへスキップ

チームにバイブコーディングを持ち込む前に整えておきたいこと

バイブコーディングをチーム開発に持ち込むと、`any`・例外の握りつぶし・フォーマット崩れがコードベースに蓄積しやすくなります。この記事では、oxlint(typeAware/typeCheck)・oxfmt・husky + lint-staged・ブランチ運用とCIを組み合わせた、Bun環境での最小構成の品質ガードをまとめています。

「AIに書かせました」というPRが、anyと型アサーションだらけで上がってくる。レビューで全部指摘していたら時間がかかるし、見逃せばコードベースは少しずつ読みにくくなっていく。バイブコーディングをチーム開発に取り入れてから、こういう場面が増えた、という話を最近よく聞きます。

この記事でやるのは、機械的に弾けるものはツールに弾いてもらい、人間はロジックのレビューに集中するための設定を整えることです。これで品質が完璧になるわけではありませんが、明らかに防げるものを取りこぼさない状態は作れます。チームで実際にやっている内容を、設定例つきで書いていきます。

この記事でやること

整えるのは次の5つです。

  1. TypeScriptの型を、逃げ道なしで使う(any・キャスト・過度な抽象化を避ける)
  2. oxlintで any やPromise忘れを自動検出する(typeAware / typeCheck を含む)
  3. oxfmtでフォーマットを統一する
  4. husky + lint-stagedでコミット前に弾く
  5. ブランチベース開発とCIチェックで、いつでもリバートできる状態を保つ

5つすべてを一気に入れる必要はありません。ですが、この順番でやると迷いにくいです。

前提環境

  • ランタイムとパッケージマネージャはBunを使います
  • TypeScriptプロジェクトで、tsconfig.json"strict": true が入っていることを前提にします
  • この記事では設定の「なぜ」を優先し、各ツールは最小構成で紹介します

1. TypeScriptの型を、逃げ道なしで使う

any とキャストは技術的負債の入り口です

TypeScriptを使っていても、any と型アサーション(as)が増えると、型の恩恵はほとんど消えます。any は伝播します。一度 any を受け取った値は、そこから先で型チェックが効かなくなり、IDEの補完も型エラーも沈黙します。

AIが生成したコードは、型が合わないときに any やキャストで「とりあえず通す」方向に倒れがちです。動いてはいるので、レビューでも見落とされやすいです。これが積み重なると、半年後に「この関数、何を受け取るんだっけ」と誰も分からなくなります。

方針はシンプルで、any の代わりに unknown を使い、型ガードで絞り込む。キャストは「自分のほうが型を分かっている」と断言する行為なので、本当に必要なときだけにします。

// ❌ anyは伝播する。ここから先、型チェックは効かない
function parse(input: any) {
  return input.data.items; // 補完も型エラーも出ない
}

// ✅ unknownで受けて、型ガードで絞り込む
function parse(input: unknown) {
  if (
    typeof input === "object" &&
    input !== null &&
    "data" in input
  ) {
    // ここから先は型が効く
  }
}

過度な抽象化を避ける

「将来使うかもしれない」で先回りして抽象化したコードは、たいてい読みにくくなります。ジェネリクスが何段も重なった関数は、型推論が追いつかなくなり、結局呼び出し側で as を書く羽目になります。

個人的な目安として、ジェネリクスが3段以上になりそうなときは、一度立ち止まるようにしています。抽象化は「2回以上同じものが出てきてから」で十分間に合います。

雑なtry/catchで例外を握りつぶさない

AIが生成したコードでよく見るのが、関数の処理全体を1つの try/catch で囲み、catch した内容を文字列にして潰してしまうパターンです。

// ❌ 例外を握りつぶしている
async function parseConfig(raw: string) {
  try {
    const data = JSON.parse(raw);
    const normalized = normalize(data);
    return validate(normalized);
  } catch (error) {
    // errorを文字列に押し込めて、詳細を消し飛ばしている
    console.error(`パース処理でエラーが発生: ${error}`);
    return null;
  }
}

これは一見「丁寧にエラーハンドリングしている」ように見えますが、実際には問題を悪化させます。理由は3つあります。

第一に、例外の詳細が消えます。error${error} のように文字列へ変換すると、スタックトレースや元の型情報が失われます。後からログを見ても「パース処理でエラーが発生」としか分からず、どこで何が起きたのか追えません。

第二に、失敗に気づきにくくなります。catch して null を返してしまうと、呼び出し側は「失敗したこと」自体を知らないまま処理を続けます。バグが別の場所で、別の形で表面化するため、原因の特定が遠回りになります。

第三に、根本解決ができなくなります。エラーが出た場所に握りつぶしのパッチを当てるのは、症状を隠しているだけです。やるべきは、そもそもそのエラーが発生しない状態にすることです。

基本方針は、例外はそのまま発生させることです。握りつぶさずに上まで伝播させたほうが、ログにスタックトレースがそのまま残り、原因にまっすぐたどり着けます。

そのうえで、

  • 失敗し得る処理は、呼び出し側で .catch() を使うか、本当に例外を避けられない箇所でだけ限定的に try/catch する
  • 握りつぶすのではなく、意味のある console.warn を出す(「何が起きたか」「どう続行するか」が分かる粒度で)

雑に握りつぶして console.error(error) するだけのコードは、書いても意味がありません。重要なのは、エラーに適当なパッチを当てることではなく、根本原因を特定して、エラーが起きない状態を作ることです。

コード品質は、できる限りパッケージに押し付ける

AIは、パースやバリデーション、日付処理などを自前で実装したがる傾向があります。動くものは出てきますが、エッジケースの抜けやメンテナンスコストが、あとから自分たちの負債になります。

ここは、よくできたメジャーなパッケージに任せたほうが安全です。

  • バリデーションやパースには、Zodのような検証ライブラリを使う
  • 日付の操作・整形には date-fns を使う(AIは日付処理を自前で書きがちですが、タイムゾーンやうるう年の罠が多い領域です)
  • Reactのフックは usehooks-ts のような実績のあるものを使う

もちろん、完璧に自前実装でき、メンテナンスし続けられるなら、パッケージは余計な依存関係になり得ます。ですが、現実的にメンテナンス性を考えるなら、広く使われ、十分にテストされたパッケージを積極的に導入したほうが、結果的に品質は安定します。どれだけコード品質をパッケージ側に押し付けられるかが、自前のコードの負債を減らす鍵になります。

2. oxlintで any とPromise忘れを自動検出する

oxlintとは

oxlintは、Rust製のツールチェイン oxc が提供する次世代Linterです。公式ベンチマークでは構文ルールにおいてESLintの約30倍高速とされており、2025年12月のv1.31.0時点で600以上のルールが実装されています。

人間がレビューで毎回「any使わないで」「このPromise、awaitしてないよ」と指摘するのは消耗します。それを機械にやらせるのが、ここの目的です。

インストールと基本設定

bun add -D oxlint

プロジェクトルートに .oxlintrc.json を置きます。まずは構文だけで効くルールから。

{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": ["typescript"],
  "rules": {
    "typescript/no-explicit-any": "error",
    "typescript/no-non-null-assertion": "error"
  }
}

これだけで、any の明示的な使用と、!(非null断言)がコミット前に弾けるようになります。

typeAware: true でPromise忘れまで拾う

ここがこの記事でいちばん推したい部分です。

従来、no-floating-promises(awaitし忘れたPromise)のような「型情報がないと判定できないルール」は、Rust製の高速Linterでは扱えませんでした。oxlintは tsgolint という別コンポーネントを使うことで、これを解決しています。

仕組みとしては、oxlint本体(Rust)がファイル探索・設定・構文ルールを担当し、tsgolint(Go)が typescript-go を使って型情報を構築し、型を使うルールを実行する、という分業構造です。

型を使うルールを有効にするには、追加のパッケージが必要です。

bun add -D oxlint-tsgolint@latest

そして .oxlintrc.json のルート設定で typeAware を有効にします。

{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": ["typescript"],
  "options": {
    "typeAware": true
  },
  "rules": {
    "typescript/no-explicit-any": "error",
    "typescript/no-non-null-assertion": "error",
    "typescript/no-floating-promises": "error",
    "typescript/no-misused-promises": "error",
    "typescript/no-unsafe-assignment": "warn"
  }
}

これで、await し忘れたPromiseや、any 型の値を安全でない場所に代入しているコードまで検出できるようになります。型を使うルールは、typescript-eslintの61ルール中59ルールをカバーしています(2025年12月のアルファ版時点)。

注意点

  • typeAware / typeCheck はルート設定でのみ指定できます。ネストした設定では指定しないでください。
  • tsgolintは typescript-go 上に成り立っているため、TypeScript 7.0以降が必要です。baseUrl など一部の古い tsconfig オプションは非対応です。
  • 執筆時点ではアルファ版で、非常に大きなリポジトリではメモリ使用量が大きくなる場合があります。導入前に手元のリポジトリで一度試すことをおすすめします。

typeCheck: truetsc --noEmit を一本化する

typeCheck を有効にすると、Lintと同時にTypeScriptの型エラーも報告されます。つまり、CIで別途 tsc --noEmit を回していたステップを、oxlintに一本化できます。

{
  "options": {
    "typeAware": true,
    "typeCheck": true
  }
}

CIのコマンドはこう変わります。

# before
tsc --noEmit
oxlint

# after
oxlint --type-aware --type-check

型チェックとLintが1コマンドで済むので、CIの構成がシンプルになります。

3. oxfmtでフォーマットを統一する

oxfmtとは

oxfmtは、同じoxc製のフォーマッターです。公式ベンチマークではPrettierの約30倍、Biomeの約2倍高速とされており、Prettier互換のワークフローで動きます。

フォーマットは、チームで1つに決めてしまえば、それ以上議論する必要のないものです。oxfmtには、これまで外部プラグインが必要だった機能が標準で入っています。

  • import文の並び替え
  • Tailwind CSSクラスの並び替え
  • package.json のフィールド並び替え

インストールと最小設定

bun add -D oxfmt

設定も最小限から始めて問題ありません。Prettierから移行する場合も、CLIの作法がPrettierに近いので、既存のスクリプトはほぼそのまま流用できます。

4. husky + lint-staged でコミット前に弾く

ここまでに用意したoxlintとoxfmtを、「コミットの関門」として自動で走らせます。手元でコミットする時点で弾ければ、レビューは意味のある会話だけに集中できます。

bun add -D husky lint-staged
bunx husky init

.husky/pre-commit に、ステージされたファイルだけをチェックするコマンドを書きます。

bunx lint-staged

package.json(または lint-staged の設定ファイル)に、対象と実行内容を書きます。

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "oxlint --type-aware",
      "oxfmt --check"
    ]
  }
}

これで、any やフォーマット崩れを含むコミットが、そもそも作られにくくなります。

ただし、これは「最終防衛線」ではありません

ここは正直に書いておきます。husky / lint-staged のチェックは、git commit --no-verify(短縮形は -n)で簡単にバイパスできます。

急いでいるとき、あるいは「これは通したい」と思ったとき、人はフックを飛ばします。悪意がなくても起こります。つまり、pre-commitフックは「うっかりを減らす仕組み」ではあっても、「絶対に通さない仕組み」ではありません。

だからこそ、最終的な品質ゲートはCIに置くべきです。次のセクションで触れます。

5. ブランチベース開発 + CIチェックで、リバートできる状態を保つ

CIを最終防衛線にする

pre-commitフックがバイパス可能である以上、誰がどうコミットしても必ず通る場所、つまりCIで同じチェックを回します。GitHub Actionsなら、こういったジョブを1つ用意しておくイメージです。

- run: bun install
- run: bunx oxlint --type-aware --type-check
- run: bunx oxfmt --check

そして、このCIが通らないとmainにマージできないように、リポジトリ側で設定します(GitHubの場合はブランチ保護ルールで「ステータスチェックの必須化」)。ここまでやって、ようやく「低品質なコードがmainに入らない」状態になります。

ブランチを切り、小さくマージする

最後は運用の話です。mainへの直pushは禁止し、変更は必ずブランチを切ってPRにします。マージはsquash mergeにして、1機能 = 1コミットにまとめます。

これをやっておくと、リバートが怖くなくなります。コミットが小さくきれいに分かれていれば、「この機能だけ戻したい」が git revert 一発で済みます。

バイブコーディング時代に一番効いたのは、実はこれかもしれません。AIに任せた変更は、後から「やっぱり違った」となることが普通にあります。いつでも簡単に戻せる状態を保っておくことが、速く試すことの前提条件になります。

まとめ

整理すると、any・キャスト・過度な抽象化を避けてTypeScriptの型を活かし、例外は握りつぶさずそのまま発生させ、バリデーションや日付処理はZodやdate-fnsのようなパッケージに任せる。oxlintの typeAware / typeCheck でPromise忘れや型エラーまで自動で拾い、oxfmtでフォーマットを統一する。それをhusky + lint-stagedでコミット前に弾きつつ、バイパスできることを前提にCIを最終ゲートとして置き、ブランチ運用でいつでもリバートできるようにしておく。やっているのは、おおむねこれだけです。

完璧なコードベースを目指しているわけではありません。機械的に防げるものを取りこぼさないこと、そして問題が起きたときに原因へたどり着ける状態を保つこと。バイブコーディングで速く書けるようになったぶん、この最低ラインを先に整えておくと、後から困らずに済みます。


参考リンク