この記事の目次
動いていたコードでstrictを有効にしたら、引数に赤線が付いた。とりあえずanyを足すと赤線は消えますが、呼び出し元の不適切な値まで通る状態が残ります。まず、値が入ってくる関数の入口へ契約を書き、その後に現れる診断を順に読みます。
対象は、関数と型注釈を読める人です。架空の展示案内の名前をURLの検索部分から取り出す処理を、厳密な型検査へ移します。Node.js 24系、比較用に固定したTypeScript 5.9.3を使います。大規模な移行を一度に行う例ではなく、一つの関数で修正の順序を確かめます。
最初の赤線が、問題の全部とは限らない
strictは一つの診断だけを増やす設定ではありません。公式のstrictの説明にあるように、複数の検査をまとめて有効にします。今回見るのは、推論できない引数を検出するnoImplicitAnyと、nullを区別するstrictNullChecksです。
引数がanyなら、文字列でもnullでも渡せてしまいます。そこで先に「文字列を受け取る」と書くと、今まで隠れていた呼び出し元のnullが見つかります。修正後に別の行へ赤線が移ることは、必ずしも修正の失敗ではありません。値の受け渡しが、より詳しく検査されるようになった可能性があります。
| 段階 | 関数の引数 | 見つかること |
|---|---|---|
| strictなし | 型注釈なし | nullを渡す呼び出しも通る |
| strictあり | 型注釈なし | 引数が暗黙のanyという診断 |
| 入口へ型を追加 | string | 呼び出し元のstringまたはnullを渡せない |
| 欠落時の仕様を追加 | stringまたはnull | 分岐後の文字列操作を検査できる |
この表は診断の数を減らす計画ではありません。「何を受け取り、欠けていたら何を返すか」を確定する順序です。unknownで受信値を検査する記事と違い、ここではAPIが返す型を利用し、既存関数の曖昧な入口を直します。
緩い設定で通る、短いファイルを用意する
空の練習フォルダで次を実行します。既存の制作物の設定を直接切り替える前に、同じ現象を小さく再現してください。
npm init -y
npm install --save-dev --save-exact typescript@5.9.3
tsconfig.jsonを次の内容にします。対象をtitle.tsに絞り、初めは出力を作らず検査だけを行います。DOMはURLSearchParamsの型を使うために含めています。
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022", "DOM"],
"strict": false,
"noEmit": true,
"noEmitOnError": true
},
"include": ["title.ts"]
}
title.tsには次を書きます。URLSearchParamsへ渡しているのは検索部分です。getは指定した項目がなければnullを返します。
function normalize(raw) {
return raw.trim();
}
function heading(params: URLSearchParams): string {
return normalize(params.get("name")) || "名称未入力";
}
for (const query of ["name=%20森の展示%20", "name=", ""]) {
console.log(heading(new URLSearchParams(query)));
}
npx tsc -p .はこの緩い設定では通ります。しかし、三件目はnullのtrimを読もうとするため、実行すれば止まります。型検査が見ていない条件を、検査成功だけで正常と判断しないことが出発点です。URLの未指定と空欄の意味は検索条件を復元する記事でも比較できます。
入口の型を決めてから、欠落時の処理を足す
tsconfig.jsonのstrictだけをtrueへ変え、同じnpx tsc -p .を実行します。最初の期待結果はTS7006です。rawが暗黙のanyであることを指摘します。noImplicitAnyの資料に対応する診断です。
次に引数だけをraw: stringへ変えます。するとheadingの呼び出しにTS2345が現れます。getの結果はstringかnullなので、文字列だけを要求する関数へそのまま渡せません。strictNullChecksの説明のとおり、nullの可能性が型へ残るためです。
今回は「項目がなければ名称未指定」「項目はあるが空なら名称未入力」とします。この区別をnormalizeへ集め、title.ts全体を次へ置き換えます。
function normalize(raw: string | null): string {
if (raw === null) return "名称未指定";
return raw.trim() || "名称未入力";
}
function heading(params: URLSearchParams): string {
return normalize(params.get("name"));
}
for (const query of ["name=%20森の展示%20", "name=", ""]) {
console.log(heading(new URLSearchParams(query)));
}
型検査に成功したら、出力を有効にして変換します。二つ目のコマンドが成功したことを確認してから実行へ進みます。
npx tsc -p .
npx tsc -p . --noEmit false --outDir out
node out/title.js
期待する三行は「森の展示」「名称未入力」「名称未指定」です。nullを許可する型へ変えるだけでは足りず、trimの前に分岐したことが動作の修正になります。
anyと感嘆符で消した赤線を、欠落入力で見直す
一つ目の失敗は、最初のrawへ明示的にanyを付けることです。noImplicitAnyは暗黙のanyを検出するので、明示したanyでは同じ診断が消えます。しかし三件目の実行は引き続き失敗します。修正ではstringまたはnullという型と分岐へ戻し、三行とも表示されることを再確認します。
二つ目は、rawをstringにしたまま、呼び出しをnormalize(params.get("name")!)とすることです。末尾の感嘆符は、nullでないという判断を型検査へ伝える非nullアサーションです。nullを文字列へ変換したり、その場で検査したりはしません。型検査が通っても、項目のない入力で停止します。
修正は、値が存在する根拠を処理に書くことです。今回は関数で欠落を扱うため感嘆符を外します。逆に名称必須の仕様なら、入口で欠落を拒否し、正常時だけ文字列を渡す設計でも構いません。赤線の消し方を選ぶ前に、欠落した入力をどう扱う画面なのか決めてください。
設定が効く範囲と、移行を終える条件
設定の適用はnpx tsc -p . --showConfigで確認できます。tscのコマンド資料で各指定を確認してください。strictがtrueでも、個別にstrictNullChecksをfalseと書けばその検査は無効になります。またnpx tsc title.tsのようにファイル名を直接渡すとtsconfig.jsonを読みません。今回の比較は、すべて同じプロジェクト指定で実施します。
既存プロジェクトでは、全ファイルに型注釈を増やすより、入出力が不明な関数から契約を決めます。検査対象の説明にあるように、importでつながるファイルも検査へ入るため、includeを一つにしても必ずその一枚だけで完結するとは限りません。個別の検査を一時的に無効にするなら、残す理由と解除条件を記録します。
strictは外部JSONの実行時検査を追加しません。省略可能な項目の書き込みを厳密にするexactOptionalPropertyTypesも別設定です。必要な検査を「strictに含まれるはず」と考えず、省略と値の削除を分ける記事のように用途ごとに確かめます。
最後の確認は、最初の版ではTS7006、stringだけを付けた版ではTS2345、完成版では診断なしと三行の出力、の三段階です。さらに検索部分をname=%20%20へ変え、名称未入力になることを確かめます。JavaScriptの無料レッスンで作った関数へ適用するときも、通常入力と欠落入力を一組にして移行の完了を判定してください。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。