Back

TypeScriptにおける型の絞り込みを理解する

TypeScriptにおける型の絞り込みを理解する

型ガードを書いたのに、TypeScriptがプロパティが存在しないと文句を言う。あるいは配列をフィルタリングしたのに、結果の型がユニオン型のまま。これらのフラストレーションは、絞り込みがどう機能すると考えているかと、TypeScriptの制御フロー解析が実際にどう動作するかのギャップから生じています。

この記事では、TypeScriptの型の絞り込みについて明確なメンタルモデルを構築します—コンパイラがコード全体で型をどう追跡し、いつその情報を失うのかを解説します。

重要なポイント

  • 型の絞り込みは、TypeScriptの制御フロー解析が検証可能なランタイムチェックに基づいて、実行パスを通じて型を追跡する仕組みです
  • 主要な絞り込みメカニズムには、typeofinstanceof、真偽値チェック、等価性チェック、in演算子、判別可能なユニオンがあります
  • 型述語を使用したユーザー定義型ガードはカスタム絞り込みを可能にしますが、TypeScriptは述語ロジックを検証しません
  • never型はコンパイル時の網羅性チェックを可能にし、未処理のケースを検出します
  • 絞り込みは、コールバック境界、プロパティの再代入、複雑なエイリアスパターンで機能しなくなります

制御フロー解析の仕組み

TypeScriptの絞り込みは魔法ではありません。コンパイラがコードの実行パスを追跡し、検証可能なランタイムチェックに基づいて型情報を更新しているのです。

条件分岐を書くと、TypeScriptは各分岐で何が真であるべきかを解析します:

function process(value: string | number) {
  if (typeof value === 'string') {
    // TypeScriptは認識: ここでvalueはstring
    return value.toUpperCase()
  }
  // TypeScriptは認識: ここでvalueはnumber
  return value.toFixed(2)
}

コンパイラはtypeofチェックを確認し、それを絞り込み構文として認識し、それに応じて型を絞り込みます。これはいくつかのJavaScript演算子に対して自動的に行われます。

主要な絞り込みメカニズム

typeofとinstanceof

typeof演算子はプリミティブ型を確実に絞り込みます。TypeScriptはその癖を理解しています—typeof null"object"を返すことも含めて。

function handle(x: unknown) {
  if (typeof x === 'string') return x.length
  if (typeof x === 'number') return x.toFixed(2)
}

クラスインスタンスの場合、instanceofが同様の絞り込みを提供します:

if (error instanceof TypeError) {
  console.log(error.message)
}

真偽値チェックと等価性チェック

真偽値チェックはnullundefinedを除外します:

function greet(name: string | null) {
  if (name) {
    return `Hello, ${name}`  // nameはstring
  }
}

等価性チェックはリテラル型に絞り込みます:

type Status = 'pending' | 'complete' | 'failed'

function handle(status: Status) {
  if (status === 'complete') {
    // statusは'complete'
  }
}

in演算子と判別可能なユニオン

in演算子はプロパティの存在をチェックし、ダックタイピングパターンを可能にします:

if ('radius' in shape) {
  return Math.PI * shape.radius ** 2
}

判別可能なユニオンは、これをリテラル型と組み合わせて強力な絞り込みパターンを実現します:

type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string }

function handle<T>(result: Result<T>) {
  if (result.success) {
    return result.data  // TypeScriptはdataが存在することを認識
  }
  return result.error   // TypeScriptはerrorが存在することを認識
}

ユーザー定義型ガード

組み込みチェックでは不十分な場合、型述語を使用してカスタムガードを定義できます:

function isString(value: unknown): value is string {
  return typeof value === 'string'
}

1つの注意点: TypeScriptは述語ロジックを検証しません。関係性をアサートしているだけです—チェックが間違っていれば、型も間違ったものになります。

モダンなTypeScript(5.5+)ではここが改善されています。Array.filterは、シンプルなコールバックに対して型述語を自動的に推論できるようになり、よくある問題点が解消されました:

const mixed: (string | number)[] = ['a', 1, 'b', 2]
const strings = mixed.filter(x => typeof x === 'string')
// TypeScriptは多くの場合string[]を推論

neverによる網羅性チェック

never型はコンパイル時の網羅性チェックを可能にします:

type Circle = { kind: 'circle'; radius: number }
type Square = { kind: 'square'; side: number }
type Triangle = { kind: 'triangle'; base: number; height: number }
type Shape = Circle | Square | Triangle

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2
    case 'square': return shape.side ** 2
    case 'triangle': return 0.5 * shape.base * shape.height
    default:
      const _exhaustive: never = shape
      return _exhaustive
  }
}

新しいshapeバリアントを追加すると、TypeScriptはneverへの代入でエラーを出し、それを処理するよう強制します。

絞り込みでないもの

絞り込みとよく混同される2つの構文について明確にしておく必要があります:

型アサーション(as) は型システムを完全にバイパスします。絞り込みではなく、上書きです。

satisfies演算子 は式が型に一致することを検証しますが、型を変更しません。ミスを検出するのに便利ですが、絞り込みメカニズムではありません。

絞り込みが機能しなくなるとき

TypeScriptの制御フロー解析には限界があります。絞り込みは以下の場合に持続しません:

  • コールバック境界を越える場合(ただし、TypeScript 5.4+ではconst変数を使用したクロージャで絞り込みが保持されるようになりました)
  • チェック間でのプロパティの再代入
  • 複雑なエイリアスパターン

絞り込みが予期せず失敗する場合は、コンパイラがチェックから使用箇所まで制御フローを実際に追跡できるかどうかを確認してください。

まとめ

絞り込みは、TypeScriptがランタイムチェックを監視し、それに応じて知識を更新していると考えてください。コンパイラは保守的です—絞り込みが健全であることを証明できる場合にのみ絞り込みを行います。

TypeScriptが追跡できるチェックを書きましょう。型アサーションよりも判別可能なユニオンを優先しましょう。網羅性チェックを使用して、実行時ではなくコンパイル時に未処理のケースを検出しましょう。

目標は型システムと戦うことではなく、絞り込みが自然に機能するようにコードを構造化することです。

よくある質問

TypeScriptの制御フロー解析は、コールバック境界を越えて絞り込みを持続させません。これは、コールバックが後で実行される可能性があり、その時点で変数の型が変わっている可能性があるためです。回避策として、コールバックの前に絞り込まれた値をconst変数に代入するか、コールバック内で型ガードを使用してください。

型ガードは、TypeScriptが認識して型を安全に絞り込むために使用するランタイムチェックです。'as'を使用した型アサーションは、ランタイム検証なしで値を特定の型として扱うようTypeScriptに指示します。型ガードは実際のチェックを伴うため安全ですが、アサーションは仮定が間違っている場合にバグを隠す可能性があります。

'kind'や'type'のような共通のリテラルプロパティを共有する関連型がある場合は、判別可能なユニオンを使用してください。switch文での自動絞り込みを提供し、網羅性チェックを可能にします。型ガードは、外部データを検証する場合や、作業している型を変更できない場合に適しています。

これは通常、TypeScriptが絞り込みを追跡できなくなったことを意味します。一般的な原因には、プロパティをチェックしてから別の参照を通じてアクセスする、チェックと使用の間で変数を再代入する、チェックが異なるスコープで行われる、などがあります。型チェックを値を使用する場所の近くに移動するか、絞り込まれた値を新しいconst変数に格納してください。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay