soundness-traps について
soundness-traps は、実行時の値が静的な型と異なる場合を理解するための TypeScript 開発者向けスキルです。よくある落とし穴、防御的なコーディング、関連するコンパイラーオプションを扱います。
主な機能
- 01配列の未検査アクセスのリスクを解説
- 02型アサーションと any の落とし穴を紹介
- 03コールバックと変更の危険性を説明
- 04外部データの検証を推奨
- 05厳格なコンパイラーオプションを解説
活用シーン
- 01一見あり得ない実行時エラーの調査
- 02TypeScript の型安全性リスクの確認
- 03防御的なコードの作成
- 04厳格なコンパイラーオプションの検討
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill soundness-trapsインストール後、Claude Code で/soundness-trapsを入力して読み込まれたか確認します。
詳細
型の健全性に関する落とし穴を避ける
概要
TypeScript は健全ではありません。実行時の値が静的型と食い違うことがあります。
「健全性」とは、静的型が常に実行時の値と一致することを意味します。TypeScript は利便性のために、意図的にある程度の健全性を犠牲にしています。よくある落とし穴を把握しておきましょう。
このスキルを使う場面
- 「起こるはずのない」実行時エラーをデバッグするとき
- TypeScript の制限を理解するとき
- 防御的なコードを書くとき
- 厳格なオプションのトレードオフを評価するとき
絶対のルール
TypeScript types are NOT runtime guarantees.
Know the common soundness traps and avoid them.
覚えておきましょう:
- 配列へのアクセスでは境界チェックが行われません
- 型アサーションは型チェックを回避します
- 関数はパラメーターを変更することがあります
- 外部データは宣言された型に一致しないことがあります
健全性と利便性
TypeScript は多くの場合、健全性よりも利便性を優先します。
const xs = [1, 2, 3];
const x = xs[10];
// ^? number (but actually undefined!)
これは健全ではありませんが、便利です。アクセスのたびに境界をチェックするのは面倒だからです。
よくある健全性の落とし穴
1. チェックされない配列アクセス
const arr = [1, 2, 3];
const item = arr[5]; // undefined at runtime
// ^? number (wrong!)
item.toFixed(2); // Crashes!
解決策: noUncheckedIndexedAccess を使うか、明示的にチェックします。
const item = arr[5];
if (item !== undefined) {
item.toFixed(2); // OK
}
2. 型アサーション
const hour = (new Date()).getHours() || null;
// ^? number | null
// Assertion removes null
const definitelyHour = hour as number;
// ^? number (but might be null!)
解決策: アサーションの代わりに条件分岐を使います。
if (hour !== null) {
hour.toFixed(1); // TypeScript knows it's number
}
3. any 型
function log(x: number) {
console.log(x.toFixed(1));
}
const val: any = 'not a number';
log(val); // No error, crashes at runtime
解決策: any を避けます。ナローイングと組み合わせて unknown を使います。
function log(x: unknown) {
if (typeof x === 'number') {
console.log(x.toFixed(1));
}
}
4. オブジェクトのインデックスアクセス
type Dict = { [key: string]: string };
const dict: Dict = { a: 'apple' };
const val = dict['b'];
// ^? string (but actually undefined!)
解決策: 型に undefined を含めます。
type Dict = { [key: string]: string | undefined };
const val = dict['b'];
// ^? string | undefined
5. 関数パラメーターの変更
function addFox(animals: Animal[]) {
animals.push(new Fox());
}
const hens: Hen[] = [new Hen()];
addFox(hens); // Fox in the henhouse!
解決策: 変更を防ぐために readonly を使います。
function addFox(animals: readonly Animal[]) {
animals.push(new Fox());
// ~~~~ Property 'push' does not exist
}
6. コールバックによって無効になる絞り込み
interface Data {
value?: string;
}
function process(data: Data, callback: (d: Data) => void) {
if (data.value) {
callback(data);
console.log(data.value.toUpperCase()); // Might crash!
// ^? string (but callback might have deleted it!)
}
}
process({ value: 'hello' }, d => delete d.value);
解決策: コールバックの前に値を取得しておきます。
function process(data: Data, callback: (d: Data) => void) {
const value = data.value;
if (value) {
callback(data);
console.log(value.toUpperCase()); // Safe!
}
}
7. 不正確な型宣言
// Library types might be wrong
declare function getUser(): { name: string; email: string };
const user = getUser();
user.email.toLowerCase(); // Might crash if email is undefined!
解決策: 外部データを実行時に検証します。
const user = getUser();
if (user.email) {
user.email.toLowerCase();
}
8. オプショナルプロパティと代入可能性
interface Person { name: string; }
interface AgePerson { name: string; age?: number; }
const p: Person = { name: 'Bob', age: '30' }; // age is string
const ap: AgePerson = p; // No error!
console.log(ap.age?.toFixed()); // toFixed on string!
これは、TypeScript の構造的型付けに起因する、微妙な非健全性です。
健全性のためのコンパイラーオプション
| オプション | 検出できる内容 |
|---|---|
strictNullChecks | null/undefined の代入 |
noUncheckedIndexedAccess | undefined を返す配列/オブジェクトへのアクセス |
strictFunctionTypes | 関数パラメーターの反変性 |
利便性は多少犠牲になりますが、より安全にするため、これらを有効にしましょう。
一般的な戦略
- 外部データを検証する(API、JSON、ユーザー入力)
- 変更しないパラメーターには readonly を使う
- any より unknown を使う
- 型アサーションを避け、代わりにナローイングを使う
- 厳格なコンパイラーオプションを有効にする
- エッジケースをテストする(空の配列、存在しないプロパティ)
プレッシャーへの対処プロトコル
1. 「TypeScript なら検出できるはず」
プレッシャー:「なぜ TypeScript はこのバグを検出しなかったのですか?」
回答: TypeScript は多くの場合、意図的に非健全です。
行動: 落とし穴を把握し、防御的なコードを書きます。
2. 「厳しすぎる」
プレッシャー:「noUncheckedIndexedAccess は面倒です」
回答: 実際のバグを検出できます。その手間には価値があります。
行動: 厳格なオプションを有効にし、エッジケースに対処します。
危険信号 — STOPして再検討
- 境界をチェックせずに配列要素にアクセスする
- 型エラーを「修正」するために型アサーションを使う
- 関数パラメーターを変更する
- 外部データが宣言された型に一致すると信頼する
よくある正当化(すべて誤り)
| 言い訳 | 実態 |
|---|---|
| 「TypeScript が検出してくれる」 | TypeScript は健全ではない |
| 「常に値が入っている」 | そうでなくなるまでは |
| 「API のドキュメントはよく整備されている」 | ドキュメントは誤っていることがある。検証しましょう |
クイックリファレンス
// TRAP: Array access
const x = arr[10]; // Might be undefined
// TRAP: Type assertion
const y = val as number; // Might not be number
// TRAP: any type
const z: any = 'string';
z.toFixed(); // Crashes
// TRAP: Index signature
const v = dict['missing']; // Undefined
// SAFE: Narrowing
if (typeof val === 'number') { val.toFixed(); }
// SAFE: Optional chaining
arr[10]?.toFixed();
// SAFE: undefined in type
type Dict = { [k: string]: string | undefined };
要点
TypeScript は便利ですが、健全ではありません。
型が実行時の値と食い違う可能性のある箇所を把握しましょう。厳格なオプションを有効にし、外部データを検証しましょう。防御的なコードを書き、型を盲信しないでください。
参考資料
Dan Vanderkam 著『Effective TypeScript』項目 48「Avoid Soundness Traps」に基づいています。