定義
型契約の前提環境とは、「このコードはコンパイルできない」という判定が成り立つために必要なコンパイラ設定とバージョンのことです。
肝心なのはここです。「コンパイルできない」はコードの性質ではなく、tsconfig の関数です。 同じファイルがあるリポジトリでは拒否され、別のリポジトリでは通ります。だから型で何かを防いだと言うなら、その判定がどんな環境で出たものなのかも一緒に固定しておく必要があります。
なぜ必要か
型契約を作って「これでこういう間違ったコードはコンパイルできません」と言ったとしましょう。でもその言葉が本当かどうかは設定次第です。
function greet(name: string) {
return name.toUpperCase();
}
greet(null); // 防げるでしょうか?
strictNullChecks がオフなら通ります。 null はすべての型に代入可能だからです。契約をどれだけ緻密に組んでも、前提がなければ全部無意味です。
もっと微妙なケースもたくさんあります。
const config: Record<string, string> = {};
config.apiUrl.length; // noUncheckedIndexedAccess がなければ通ります
type Options = { retry?: number };
const o: Options = { retry: undefined }; // exactOptionalPropertyTypes がなければ通ります
なので型契約を作るリポジトリでは、リポジトリごとに一度環境を確認して記録します。カードごと、作業ごとにやり直すのではなく、一度だけです。
動作原理
基準になるのは実効値です
tsconfig.json ファイルに見えている内容が実際の設定ではありません。extends のチェーンをたどりながらマージされるからです。
# ファイルに書かれた値ではなく、実際に適用される値を見ます
pnpm exec tsc --showConfig
extends: "@company/tsconfig/base" という一行の裏に何が入っているかは、このコマンドでしか分かりません。フレームワークが独自の base config を提供する場合(Next.js など)も同じです。
コンパイラのバージョンも実際に解決されたものが基準です
pnpm exec tsc --version
package.json に ^5.4.0 と書いてあっても、ロックファイルが実際に何をインストールしたのかが判定基準です。TypeScript Playground や最新バージョンでだけ通った結果は証拠になりません。エディタが使うバージョン(VS Code 内蔵の TypeScript)がプロジェクトのバージョンと違うこともよくあります。
確認する項目
| 項目 | 基準 | 満たさないと弱くなるもの |
|---|---|---|
| TypeScript バージョン | ≥ 5.4 | NoInfer(5.4)・const 型パラメータ(5.0)・satisfies(4.9)が使えません |
strict | 必須 | union の絞り込みと null 安全がないので、契約の前提そのものがありません |
strictFunctionTypes | 推奨 | コールバック引数の検査がゆるくなります(メソッドの双変性の例外はオンでも残ります) |
useUnknownInCatchVariables | 推奨 | catch の値が any のように流れ回ります |
noUncheckedIndexedAccess | 推奨 | 配列インデックスや lookup アクセスが undefined 検査なしで通ります |
noPropertyAccessFromIndexSignature | 推奨 | 開いた dictionary のキーを確定したプロパティのように読みます |
exactOptionalPropertyTypes | 推奨 | 「維持 vs 削除」の undefined の区別が保証されません |
strict は複数のフラグの束です。個別のフラグをオフにしたまま strict: true を維持しているリポジトリもあるので、束の名前ではなく実効値を見ます。
実務での適用
判定と対応
すべて満たしている — tsconfig の場所と TypeScript のバージョンを記録して進みます。以降の作業で再検証はしません。
strict またはバージョンが未達 — ここで止まります。tsconfig を黙って変えません。 リポジトリ全体に波及するポリシー変更なので、オンにした瞬間に既存ファイル数百個でエラーが噴き出すことがあります。未達の項目と影響範囲をまとめて、人に判断を委ねます。
推奨フラグが未達 — オンにする変更を提案しつつ、却下されたり保留されたら弱くなる契約のリストを記録して進みます。 「これはコンパイルではなくレビューとテストが捕まえるべきだ」という意味だからです。
記録しておくこと
## 型環境(2026-08-24 確認)
- TypeScript: 5.6.3(ロックファイルの解決基準)
- tsconfig の実効値: `tsc --showConfig` 基準
- strict: true
- strictFunctionTypes: true
- noUncheckedIndexedAccess: true
- exactOptionalPropertyTypes: **false** ← 未達
- 弱くなる契約: 演算 union の「維持 vs 削除」の区別がコンパイルで保証されない
→ 該当する mutation の経路はランタイムテストが所有する
コンパイラのアップグレードはポリシー変更です
TypeScript のバージョンを上げるのは単なるツール更新ではありません。assignability の判定や strict 系の挙動が変わると、以前は通っていたコードが拒否されたり、その逆になったりします。 記録しておいた環境と違うバージョンが diff に現れたら、この確認をやり直して記録を更新します。
性能の証拠が必要な場合
再帰型や分配 conditional 型を導入すると、コンパイル時間が目に見えて伸びることがあります。そのときは数値の上限をドキュメントにハードコードせず、実際のプロジェクトのコンパイラ診断で判定します。
pnpm exec tsc --noEmit --extendedDiagnostics
導入前後の値を比べて記録します。--generateTrace は悪化が実際に観測されて原因の特定が必要なときだけ使います。
トレードオフ
一度の確認コストがかかります。 コマンドを二つ回して表を埋める作業なので 10 分もあれば終わりますが、急いでいるときは飛ばしたくなります。
飛ばすとどうなるかというと — 型契約を頑張って作ったのに、あとで strict がオフだったと気づきます。その間「防いだ」と信じていたものが全部防げていなかったわけです。これは巻き戻すのが高くつきます。
フラグをオンにするのはもっと高くつきます。 noUncheckedIndexedAccess を一つオンにするだけでも、既存コードで数百個のエラーが出ることがあります。だから「推奨フラグがオフなら無条件にオンにする」ではなく、オンにするのを提案して、却下されたら弱くなるものを記録するのが現実的な折衷案です。
使ってはいけない場合
- 型契約を作らないリポジトリ。 スクリプトやプロトタイプにこの手順を適用すると、形式が増えるだけです。
- 作業のたびに繰り返すこと。 リポジトリごとに一度です。tsconfig や TypeScript のバージョンが変わる diff でだけ見直します。
- 環境を直すという理由で人の承認なしに tsconfig を修正すること。 これは絶対にやりません。影響範囲がリポジトリ全体です。
よくある間違い
tsconfig.jsonファイルだけを見て判断します。extendsチェーンの裏に何があるか分からないまま結論を出してしまいます。- エディタ基準で確認します。 VS Code 内蔵の TypeScript バージョンとプロジェクトのロックファイルのバージョンが違うと、エディタでは赤いのに CI は通る、あるいはその逆になります。
- Playground で検証します。 Playground は常に最新バージョンでデフォルト設定です。リポジトリの判定の証拠にはなりません。
strict: trueだけ見て安心します。 個別のフラグが裏でオフになっていることがあります。- 未達のフラグを黙ってオンにします。 数百個のエラーと一緒にレビュアーを驚かせます。
- 弱くなる契約を記録しません。 フラグをオンにできなかったこと自体はいいのですが、そのせいで何が防げないのかを誰も知らないのが問題です。
関連概念
- type-level-testing — この環境の上で「何がコンパイルできないのか」をコードで固定する
- false-type-contracts — フラグがオフで偽になってしまう契約たち
- typescript-variance —
strictFunctionTypesをオンにしても残る穴