evolving-types について
代入に応じて推論型が変わる変数を扱う TypeScript 開発者向けのスキルです。進化する型の用途や脆弱さを解説し、明示的な型注釈で明確にする方法を示します。
主な機能
- 01進化する型の動作
- 02代入による型の絞り込み
- 03暗黙の any のリスク
- 04明示的な型注釈の指針
- 05配列の蓄積に対する関数型の代替
活用シーン
- 01未初期化変数の型付け
- 02空配列の型注釈の選択
- 03代入順に依存するコードの確認
- 04配列蓄積処理のリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill evolving-typesインストール後、Claude Code で/evolving-typesを入力して読み込まれたか確認します。
詳細
変化する型を理解する
概要
一部の変数は幅広い型で始まり、TypeScript が値の追加を認識するにつれて型が絞り込まれます。
これは、型は変化しないという規則の例外です。値を指定せずに初期化した変数や、空配列として初期化した変数は、代入した値に応じて絞り込まれる「変化する」型を持つことがあります。
このスキルを使う場面
nullまたはundefinedで初期化する変数- 空の状態から始まり、値が
pushされる配列 anyで始まり、型が絞り込まれる変数- 明示的な型注釈のほうが適している場面を理解する
絶対ルール
Evolving types work but are fragile.
Prefer explicit annotations for clarity.
覚えておいてください:
- 型付きの初期値を持たない変数にのみ適用されます
- 型は代入に応じて変化します
- 最終的な型は、すべての代入が行われた後にのみ有効です
- 明示的な型注釈のほうが分かりやすいことがよくあります
検出:変化する any
const result = []; // any[]
result.push('a'); // string[]
result.push(1); // (string | number)[]
result
// ^? (string | number)[]
型は push するたびに変化します。
変化する型の仕組み
未初期化の変数
let val; // any (evolving)
val
// ^? let val: any
if (Math.random() < 0.5) {
val = /hello/;
val
// ^? let val: RegExp
} else {
val = 12;
val
// ^? let val: number
}
val
// ^? let val: number | RegExp
TypeScript は代入を追跡し、ユニオン型を計算します。
空配列
const arr = []; // any[] (evolving)
arr.push(1);
arr
// ^? number[]
arr.push('hello');
arr
// ^? (string | number)[]
初期値が null または undefined
let x = null; // any (evolving)
x
// ^? null
x = 12;
x
// ^? number
型の変化が止まるとき
変数がスコープを抜けるか、関数内で使われると、その型は固定されます。
function buildArray() {
const arr = [];
arr.push(1);
arr.push(2);
return arr; // Type fixed as number[]
}
const myArray = buildArray();
myArray.push('hello'); // Error if return type is number[]
変化する型の問題点
順序が重要
const arr = [];
arr.push(1);
// arr is number[] here
// If this line is later:
arr.push('hello');
// arr is (string | number)[] but earlier uses assumed number[]
暗黙の any
noImplicitAny を指定すると、型注釈のない空配列は any[] になります。
const values = []; // Implicit any[] - may cause lint warnings
不安定な型推論
let x = null;
x = 'hello';
x = 42; // Now it's string | number
// Later, someone adds:
x = true; // Now it's string | number | boolean
// All code using x must handle all possibilities
より良い方法:明示的な型注釈
// Clear intent, stable type
const result: number[] = [];
result.push(1);
result.push(2);
// result is always number[]
// Prevents accidents
result.push('hello');
// ~~~~~~~
// Argument of type 'string' is not assignable to 'number'
変化する型を使ってもよい場合
短く単純なループ
const squares = [];
for (let i = 0; i < 5; i++) {
squares.push(i * i);
}
// squares: number[] is clear from context
既知の型の蓄積
let result = null;
for (const item of items) {
if (condition(item)) {
result = item;
break;
}
}
// result evolves to ItemType | null
関数型の代替手段
配列の型を変化させる代わりに、関数型の構文を使う方法を優先してください。
// Don't:
const doubled = [];
for (const n of numbers) {
doubled.push(n * 2);
}
// Do:
const doubled = numbers.map(n => n * 2);
// ^? number[]
型が直接推論されるため、変化させる必要はありません。
実例
// Evolving (works but fragile)
async function fetchData() {
let data;
try {
const response = await fetch('/api');
data = await response.json();
} catch (e) {
data = null;
}
return data; // any
}
// Better: explicit
async function fetchData(): Promise<Data | null> {
try {
const response = await fetch('/api');
return await response.json();
} catch (e) {
return null;
}
}
プレッシャーに対抗するプロトコル
1. 「変化する型は機能する」
プレッシャー:「TypeScript が自動で判断してくれます」
**回答:**不安定で、新しいコードを追加すると予期せず変わることがあります。
**アクション:**安定性を高めるため、明示的な型注釈を追加します。
2. 「型がまだ分かりません」
プレッシャー:「型は実行時の条件によって変わります」
**回答:**可能な型は分かっているはずです。それらを宣言してください。
**アクション:**ユニオン型を使います:let x: string | number | null = null;
危険信号 — 作業を止めて再検討
- 型注釈のない
const arr = [] - 型注釈のない
let x = nullまたはlet x - 代入の順序によって変わる型
- リントで
any[]の警告が出る
よくある正当化(すべて無効)
| 言い訳 | 実際のところ |
|---|---|
| 「TypeScript が推論してくれます」 | 何かは推論しますが、望んだ型とは限りません |
| 「型は後で追加します」 | その「後」は来ません。今追加してください |
| 「一時的なコードです」 | 一時的なコードは恒久的なものになります |
クイックリファレンス
// EVOLVING (works but fragile)
const arr = []; // any[]
arr.push(1); // number[]
arr.push('a'); // (string | number)[]
// BETTER (stable and clear)
const arr: number[] = [];
arr.push(1);
arr.push('a'); // Error!
// BEST (functional)
const arr = items.map(item => item.value);
// Type inferred correctly
要点
変化する型は便利な機能ですが、ベストプラクティスではありません。
TypeScript は代入を通じて型を追跡できますが、明示的な型注釈のほうが分かりやすく、堅牢です。変化する型は、単純で局所的なコードに限って使ってください。それ以外の場合は、型注釈で意図を宣言してください。
参考資料
Dan Vanderkam 著「Effective TypeScript」、項目 25「変化する型を理解する」に基づいています。