mcpField
S
soundness-traps

soundness-traps

github.com/marius-townhouse開発646 回インストール39 週間前に更新
リポジトリ

soundness-traps について

soundness-traps は、実行時の値が静的な型と異なる場合を理解するための TypeScript 開発者向けスキルです。よくある落とし穴、防御的なコーディング、関連するコンパイラーオプションを扱います。

主な機能

  1. 01配列の未検査アクセスのリスクを解説
  2. 02型アサーションと any の落とし穴を紹介
  3. 03コールバックと変更の危険性を説明
  4. 04外部データの検証を推奨
  5. 05厳格なコンパイラーオプションを解説

活用シーン

  1. 01一見あり得ない実行時エラーの調査
  2. 02TypeScript の型安全性リスクの確認
  3. 03防御的なコードの作成
  4. 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 の構造的型付けに起因する、微妙な非健全性です。

健全性のためのコンパイラーオプション

オプション検出できる内容
strictNullChecksnull/undefined の代入
noUncheckedIndexedAccessundefined を返す配列/オブジェクトへのアクセス
strictFunctionTypes関数パラメーターの反変性

利便性は多少犠牲になりますが、より安全にするため、これらを有効にしましょう。

一般的な戦略

  1. 外部データを検証する(API、JSON、ユーザー入力)
  2. 変更しないパラメーターには readonly を使う
  3. any より unknown を使う
  4. 型アサーションを避け、代わりにナローイングを使う
  5. 厳格なコンパイラーオプションを有効にする
  6. エッジケースをテストする(空の配列、存在しないプロパティ)

プレッシャーへの対処プロトコル

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」に基づいています。

コメント