type-narrowing について
制御フローによって広い型をより具体的な型へ絞り込む TypeScript のスキルです。ユニオン型や null 許容値を扱い、不要な型アサーションをチェックや型ガードに置き換える方法を説明します。
主な機能
- 01null と undefined のチェック
- 02typeof と instanceof による絞り込み
- 03判別可能なユニオン型の処理
- 04ユーザー定義型ガードの作成
- 05よくある絞り込みの落とし穴の把握
活用シーン
- 01undefined の可能性があるエラーの解消
- 02ユニオン型の値の処理
- 03条件分岐での型アサーションの置き換え
- 04null 許容の DOM 要素の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill type-narrowingインストール後、Claude Code で/type-narrowingを入力して読み込まれたか確認します。
詳細
型の絞り込みを理解する
概要
型の絞り込みとは、制御フローに基づいて TypeScript が型を広いものからより具体的なものへと絞り込むプロセスです。
絞り込みを習得すると、型アサーションを使わずによりすっきりしたコードを書けるようになり、TypeScript がコードのロジックを理解しやすくなります。
このスキルを使う場面
Type | nullまたはType | undefinedを扱うときstring | numberのようなユニオン型を処理するとき- 判別可能なユニオン(タグ付きユニオン)を処理するとき
- 「undefined の可能性があります」というエラーが発生したとき
- 条件分岐で型アサーションを避けたいとき
絶対のルール
NEVER use type assertions when narrowing would work.
例外はありません:
- 「そのほうが簡単だから」という理由では使いません
- 「すでに確認したから」という理由では使いません
- 「TypeScript が理解しないから」という理由では使いません
検出:「条件分岐内の型アサーション」という危険信号
if ブロック内で as Type を使っている場合は、代わりに型を絞り込める可能性があります。
// ❌ VIOLATION: Using assertion instead of narrowing
function process(value: string | null) {
if (value !== null) {
console.log((value as string).toUpperCase()); // Unnecessary assertion
}
}
// ✅ CORRECT: TypeScript narrows automatically
function process(value: string | null) {
if (value !== null) {
console.log(value.toUpperCase()); // value is string here
// ^? (parameter) value: string
}
}
絞り込みの手法
1. null/undefined のチェック
const el = document.getElementById('foo');
// ^? const el: HTMLElement | null
if (el) {
el.innerHTML = 'Hello';
// ^? const el: HTMLElement
} else {
el
// ^? const el: null
}
2. typeof による型ガード
function padLeft(value: string, padding: string | number) {
if (typeof padding === 'number') {
return ' '.repeat(padding) + value;
// ^? (parameter) padding: number
}
return padding + value;
// ^? (parameter) padding: string
}
3. instanceof による型ガード
function processDate(input: Date | string) {
if (input instanceof Date) {
return input.toISOString();
// ^? (parameter) input: Date
}
return new Date(input).toISOString();
// ^? (parameter) input: string
}
4. プロパティのチェック(in)
interface Bird { fly(): void; }
interface Fish { swim(): void; }
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly();
// ^? (parameter) animal: Bird
} else {
animal.swim();
// ^? (parameter) animal: Fish
}
}
5. 判別可能なユニオン(タグ付きユニオン)
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
// ^? (parameter) shape: Circle
case 'rectangle':
return shape.width * shape.height;
// ^? (parameter) shape: Rectangle
}
}
6. Array.isArray
function process(input: string | string[]) {
if (Array.isArray(input)) {
return input.join(', ');
// ^? (parameter) input: string[]
}
return input;
// ^? (parameter) input: string
}
7. 早期の throw/return
function processElement(el: HTMLElement | null) {
if (!el) {
throw new Error('Element not found');
}
// After the throw, el is narrowed
el.innerHTML = 'Hello';
// ^? (parameter) el: HTMLElement
}
ユーザー定義の型ガード
組み込みの絞り込みだけでは不十分な場合:
interface Cat { meow(): void; }
interface Dog { bark(): void; }
// Type predicate: `pet is Cat`
function isCat(pet: Cat | Dog): pet is Cat {
return 'meow' in pet;
}
function speak(pet: Cat | Dog) {
if (isCat(pet)) {
pet.meow();
// ^? (parameter) pet: Cat
} else {
pet.bark();
// ^? (parameter) pet: Dog
}
}
絞り込みでよくある落とし穴
typeof null は "object"
function process(value: string | object | null) {
if (typeof value === 'object') {
value // Still includes null!
// ^? string | object | null -> object | null
}
}
// Fix: Check null explicitly first
function process(value: string | object | null) {
if (value === null) return;
if (typeof value === 'object') {
value // Now just object
// ^? (parameter) value: object
}
}
偽値
function process(x?: number | string | null) {
if (!x) {
x // Includes 0, "", null, undefined!
// ^? string | number | null | undefined
}
}
コールバックでは絞り込みが維持されない
function processLater(value: { name?: string }) {
if (value.name) {
setTimeout(() => {
console.log(value.name.toUpperCase());
// ~~~~~~~~~ Object is possibly 'undefined'
}, 100);
}
}
// Fix: Capture the narrowed value
function processLater(value: { name?: string }) {
if (value.name) {
const name = value.name; // Capture as const
setTimeout(() => {
console.log(name.toUpperCase()); // OK
}, 100);
}
}
プレッシャーへの対処プロトコル
1. 「TypeScript がチェックを理解しない」
プレッシャー:「チェックしたのに、TypeScript が型を絞り込まない」
対応: TypeScript が理解できるパターンを使うようにチェックを書き直します。
アクション: 標準的な絞り込みパターンを使います。必要に応じて型ガードを作成します。
2. 「型アサーションのほうが簡単」
プレッシャー:「if 文の代わりに as Type を使えばいい」
対応: 型アサーションは実行時に検証しません。絞り込みなら検証できます。
アクション: チェックを記述します。コードの安全性が高まります。
危険信号 — 立ち止まって再検討する
- if/switch ブロック内の
as Type - うまく機能しない絞り込み(パターンを確認してください)
- null チェックなしの
typeof x === 'object' - 0 や "" の可能性がある値に対する偽値チェック
- 適切な null チェックの代わりに
!を使う
よくある正当化(すべて不適切)
| 言い訳 | 実際のところ |
|---|---|
| 「すでに確認した」 | TypeScript がチェックを認識しないなら、確認したことにはなりません。 |
| 「型アサーションのほうが短い」 | コードは短ければよいとは限りません。 |
| 「TypeScript が間違っている」 | TS が理解できるパターンを使うようにチェックを書き直します。 |
クイックリファレンス
| 使用する型 | 使うもの | 絞り込まれる型 |
|---|---|---|
T | null | if (x) または if (x !== null) | T |
string | number | typeof x === 'string' | string |
Dog | Cat | x instanceof Dog | Dog |
A | B(kind を持つ) | switch (x.kind) | A または B |
| 複雑なチェック | ユーザー定義の型ガード | 独自の型 |
要点
制御フローを通じて TypeScript に型を絞り込ませましょう。型アサーションで迂回しないでください。
標準的な絞り込みパターンを使い、必要に応じて型ガードを作成します。コールバックの前に値を変数へ格納します。TypeScript の絞り込みは強力です。回避するのではなく、うまく活用する方法を学びましょう。
参考文献
Dan Vanderkam 著「Effective TypeScript」項目 22「型の絞り込みを理解する」に基づいています。