Skip to content

Instantly share code, notes, and snippets.

@ryo-murai
Last active August 27, 2025 09:33
Show Gist options
  • Save ryo-murai/b06e1340bbef89dc3cda88b169ddd295 to your computer and use it in GitHub Desktop.
Save ryo-murai/b06e1340bbef89dc3cda88b169ddd295 to your computer and use it in GitHub Desktop.

Effective TypeScript 2nd Edition

第3章 型推論と制御フロー解析

項目18 推論可能な型でコードを乱雑にしない

新しいTypeScript開発者は、JavaScriptからの移行時に、すべての変数に型アノテーションを記述しがちです。しかし、TypeScriptの強力な型推論機能により、多くの型アノテーションは不要であり、コードのノイズを増やし、かえって型安全性を低下させる可能性さえあります。TypeScriptは、numberのようなプリミティブ型だけでなく、より複雑なオブジェクトや配列の型も正確に推論できます。明示的な型アノテーションを省略することで、より正確なリテラル型(例: stringではなく"y")が推論されることもあり、これが型安全性を向上させます。また、型推論に頼ることで、分割代入などのリファクタリングが容易になります。

型アノテーションが依然として必要な主な状況は以下の通りです:

  • 関数のパラメーター: TypeScriptは通常、変数の型を最初に登場したときに決定するため、関数のパラメーターには明示的な型アノテーションが必要となることが多いです(デフォルト値がある場合や、型宣言を持つライブラリのコールバックとして使用される場合は推論されることもあります).
  • 関数の戻り値の型: デフォルトでは不要とされていますが、複数のreturn文がある場合パブリックAPIの一部である場合、または戻り値で名前付きの型を使用したい場合には、実装上のエラーが呼び出し元のコードに漏れ出すのを防ぎ、関数の契約を明確にするために、戻り値の型アノテーションを追加することが推奨されます.
  • オブジェクトリテラル: オブジェクトリテラルに明示的な型アノテーションを付けることで、余剰プロパティチェックが有効になり、エラーが宣言箇所近くで報告されるようになります.

typescript-eslintno-inferrable-typesルールを使用すると、推論可能なプリミティブ型に明示的な型アノテーションを付けることを禁止できます.

項目19 異なる型には異なる変数を使う

JavaScriptでは、1つの変数を異なる型の値に再利用することが可能ですが、TypeScriptでこれをモデリングするのは難しく、予期せぬ挙動につながることがあります。そのため、意味的に異なる値には新しい変数を導入することが推奨され、型の衝突や混乱を避けるように促しています。

項目20 変数の型がどのように決まるか理解する

TypeScriptは、リテラル値からより広い型(例: "x"からstring12からnumber)を推論するプロセスを「型拡大(widening)」と呼びます。これは、TypeScriptが型の具体性と柔軟性のバランスを取ろうとする挙動です。

この型拡大の挙動を制御する方法はいくつかあります:

  • const宣言: letの代わりにconstで変数を宣言すると、より狭い型(例: "x"から"x")が推論されます.
  • 明示的な型アノテーション: 型アノテーションを追加することで、推論された型を上書きできます.
  • 文脈の利用: 値を関数の引数として渡すなど、型チェッカーに追加の文脈を与えることで、より正確な型が推論されることがあります(詳細は項目24で説明).
  • as constアサーション: 値の型を可能な限り最も狭いリテラル型として推論させる「const文脈」を適用します。これは型レベルの構文であり、実行時の値には影響しません.
  • satisfies演算子: as constと同様に型を狭くしますが、値が特定の型を満たしているかをチェックしつつ、推論された型はそのリテラル型を保持します。これにより、エラーが値の定義箇所で報告されるようになります.

項目21 オブジェクトを一度に構築する

TypeScriptでは変数の型は通常変化しないため、JavaScriptでオブジェクトを段階的に構築するパターンは、TypeScriptでは問題を引き起こしやすいです。初期値が空のオブジェクト({})の場合、TypeScriptはそれを空の型と推論するため、後からプロパティを追加しようとするとエラーになります。

この問題を解決するには、オブジェクトを一度に構築する(すべてのプロパティを最初のリテラルで指定する)か、明示的な型アノテーションを使用して、TypeScriptが空のオブジェクトからより広い型を推論しないようにする必要があります.

項目22 型の絞り込みを理解する

TypeScriptでは、変数の型はコードの場所に応じて変化することがあり、このプロセスは「制御フロー解析」として知られています. TypeScriptは、コードの実行フローに基づいて変数の型をより具体的に「絞り込む(narrowing)」ことができます。

型の絞り込みをトリガーする一般的なJavaScriptの構文には以下のものがあります:

  • if文(!elemのようなnullチェック、typeofによる型チェック、instanceofin演算子によるプロパティの有無チェック)
  • switch文(特にタグ付きユニオンと相性が良い)
  • Array.isArrayなどの標準関数
  • ユーザー定義の型ガード(User-defined type guards): value is Typeという戻り値の型述語を持つ関数を定義することで、カスタムの絞り込みロジックを実装できます.
  • TypeScript 5.5以降では、特定の条件を満たす関数において、関数の型述語が自動的に推論されるようになりました.

ただし、プロパティの型絞り込みは、関数呼び出しによって無効にされないという「実用的な選択」がTypeScriptによってなされており、これが不健全性につながる可能性もあります(詳細は項目48で説明).

項目23 エイリアスを作成したら一貫してそれを使う

ネストしたプロパティをローカル変数で扱うと、エイリアスを作成したことになる
エイリアスの型絞り込みはオリジナルのプロパティには反映されないし、その逆も然り。だから関数内で併用しないこと
プロパティを使う場合は型チェック後に変わるリスクがある(けど TypeScriptは実用的な選択によりスルーしている) この問題を避けるには readonlyによって変更不可とする

項目24 型推論に文脈がどう使われるか理解する

TypeScriptの型推論は、値が使われる**文脈(コンテキスト)**に大きく依存します。これは「文脈的型付け(contextual typing)」と呼ばれます。TypeScriptは、変数が最初に導入されたタイミングでその型を決定するのが一般的ですが、その後の使用文脈から型を推論することもあります。

文脈が失われると型エラーにつながることがあります。一般的な例は以下の通りです:

  • 文字列リテラル型: letで宣言された変数が、後から特定の文字列リテラル型を期待する関数に渡される場合、let変数にはより広いstring型が推論されてしまい、エラーになることがあります.
  • タプル型: タプル型を期待する関数に配列リテラルを渡す際に、中間変数に代入すると、タプル型ではなくより広い配列型が推論されてしまうことがあります.
  • オブジェクトリテラル: 特定の文字列リテラル型を含むプロパティを持つオブジェクト型を期待する関数に、オブジェクトリテラルを渡す際に、中間変数に代入すると、プロパティがより広いstring型に拡大されてしまい、エラーになることがあります.
  • コールバック関数: コールバック関数を他の関数にインラインで渡す場合、TypeScriptは文脈からコールバックのパラメーターの型を推論します.

これらの問題は、型アノテーションの追加や、**constアサーション(as const)**を使用して型を狭く保つことで解決できます. また、エラーを避けるために、インラインで値を使用する(中間変数に代入しない)形式が好ましい場合もあります.

項目25 進化する型を理解する

TypeScriptでは通常、変数の型は宣言時に決定され、その後は型を絞り込むことしかできません。しかし、nullundefined、または空の配列[]で初期化された変数は、後から要素が追加されることで型が「進化」する(より広くなる)という注目すべき例外があります。

例として、空の配列[]で初期化された変数は、最初はany[]型と推論されますが、number型の値がpushされるとnumber[]型へと進化します。これは絞り込みとは異なり、型のドメインが拡大されるプロセスです。この挙動を理解することで、必要な型アノテーションを減らすことができます。

ただし、進化する型は、意図しない型エラーを見逃す原因にもなりうるため、必要に応じて明示的な型アノテーションを追加することで、より厳密なチェックを行うことも検討すべきです。

項目26 関数型の標準APIやライブラリを使って型の流れを促進する

JavaScriptの標準的な関数型API(例: mapflatMapfilterreduce)や、Lodashのようなユーティリティライブラリの関数を使用することは、手書きのループよりも推奨されます。これは、これらの関数に付属するTypeScriptの型宣言が、型をコード全体に流すのを助けるためです。手書きのループでは、プログラマー自身が型を管理する必要があるのに対し、APIやライブラリの型宣言は、型推論を効率的に機能させ、コードの正確性を向上させます。

項目27 コールバックの代わりにasync関数を使用して型の流れを改善する

JavaScriptで非同期処理を扱う際、PromiseベースのAPIよりもasync/await構文を使用することが推奨されます。主な理由は以下の2点です:

  1. コードがより簡潔で分かりやすくなる
  2. async関数は常にPromiseを返すことが強制されるため、型推論が簡素化され、潜在的なバグを回避できます. たとえawaitするものがなくても、async関数はPromise型を返します.

これにより、TypeScriptの型推論の仕組みが最大限に活用され、コールバックベースのAPIで発生しがちな型関連の混乱を防ぐことができます.

項目28 クラスやカリー化を使って型パラメーターを段階的に割り当てる

複数のジェネリック型パラメーターを持つ関数において、TypeScriptの型推論は「全か無か」の性質を持ちます。つまり、すべての型パラメーターを推論させるか、またはすべてを明示的に指定する必要があります。

型パラメーターを部分的に推論させ、よりきめ細かく制御したい場合には、以下の2つの選択肢があります:

  1. クラス: ジェネリッククラスを使用することで、クラスがインスタンス化されるときに型パラメーターが設定され、その後のメソッド呼び出しで再度の型指定は不要になります.
  2. カリー化: 関数をカリー化することで、複数のジェネリック型パラメーターを段階的に割り当てることが可能になります. カリー化のアプローチは、ローカルの型エイリアスを定義できるスコープを作成するという利点があり、複雑な型表現の重複を減らすことができます.

どちらを選択するかはAPIの設計と好みに依存しますが、TypeScriptの文脈ではカリー化がローカルの型エイリアスを定義できる点で有利な場合があります.

第5章 不健全性とany型

『Effective TypeScript 第2版』の5章「不健全性とany型」では、TypeScriptの型システムが持つ漸進的かつオプション的な性質に焦点を当て、any型を賢く使用してそのデメリットを抑える方法、そしてシンボルの静的な型と実行時の型に互換性がない「不健全性」という、より一般的な問題とその回避策について説明しています。

以下に、5章の各項目を要約します。

  • 項目43 可能なかぎり狭いスコープでany型を使う any型は型チェックを効果的に無効化する強力なツールですが、その使用は慎重に行う必要があります。エラーが発生した際に、any型のスコープをできるだけ狭くすることが推奨されます。これは、@ts-expect-error@ts-ignoreといったコメントを使って特定の行のエラーを抑制したり、オブジェクト全体にas anyを適用するのではなく、問題のあるプロパティに限定してas anyを使用したりすることで実現できます。anyを返すと、その関数を呼び出すコードで型安全性が失われるため、関数からany型を返すことは避けるべきです。typescript-eslintno-unsafe-assignmentno-unsafe-returnといったルールを活用して、any型の拡散を防ぐことができます。

  • 項目44 anyをそのまま使うのではなく、より具体的な形式で使う any型はJavaScriptで表現できるすべての値を包含するため、非常に広範な型です。anyを使用する際には、本当にどんなJavaScriptの値も許容できるのかを自問し、より具体的な形式のany型、例えば**any[](任意の要素を持つ配列)、{[id: string]: any}(任意の文字列キーとany値を持つオブジェクト)、() => any(任意の戻り値を持つ関数)など**を選択することが推奨されます。これにより、ある程度の型安全性を保つことができます。また、配列の要素の型を気にしない場合は、any[]よりも安全なunknown[]の使用が好ましいとされています.

  • 項目45 安全でない型アサーションを、適切に型付けされた関数の内部に隠す (提供されたソースにはこの項目の具体的な内容が含まれていませんでした。)

  • 項目46 型が不明な値には、anyではなく unknownを使う unknown型はany型のより安全な代替手段であり、そのドメインにはJavaScriptのすべての値が含まれます。unknown型の値は、instanceofチェックやユーザー定義の型ガードなどを通じて型を絞り込むまで、操作が制限されます。これにより、any型とは異なり、コンパイラが安全でない操作を許可することを防ぎます。object{}も広い型ですが、unknownよりも少し狭い型であり、TypeScriptの型階層でunknownが最上位の「トップ型」として位置付けられています.

  • 項目47 モンキーパッチではなく、より型安全なアプローチを採用する グローバル変数やDOMにデータを保持するような、構造化されていないコードは避けるべきです。組み込みの型にデータを保持しなければならない場合は、宣言のマージ(モジュールオーグメンテーション)やカスタムインターフェイスへのアサーションといった、より型安全なアプローチを採用することを検討します。例えば、JSON.parseの戻り値の型をanyからunknownに変更したり、Setコンストラクターが文字列を受け入れる挙動を禁止したりすることができます。ただし、オーグメンテーションはスコープの問題を伴う可能性があり、実行時にundefinedの可能性がある場合はそれを含める必要があります.

  • 項目48 健全性の罠を回避する 「健全である」言語とは、すべてのシンボルの静的な型が実行時の値と互換性があることを保証する言語ですが、TypeScriptは利便性や既存のJavaScriptライブラリのモデリングを重視するため、完全に健全ではありません。不健全性はクラッシュやバグにつながる可能性があるため、避けるべきです。不健全性の一般的な原因には、any型の使用、オブジェクトや配列へのチェックなしでのアクセス、不正確な型定義、クラス階層におけるメソッドの双変性、オブジェクトや配列の変性の不正確なモデリング、関数のパラメーターの変更、オプションプロパティなどがあります。strictNullChecksnoUncheckedIndexedAccessといったコンパイラーオプションを有効にすることで、厳密さを調整し、不健全性の原因となるパターンを認識して回避することが重要です.

  • 項目49 型カバレッジを監視し、型安全性のリグレッションを防ぐ noImplicitAnyを有効にしても、明示的なany型やサードパーティの型宣言(@types)を通じて、any型がコードに入り込む可能性があります。型安全性や開発者体験を損なうany型の問題を監視するために、type-coverageのようなツールを使用することが推奨されます。type-coverageは、プロジェクト内のany型以外の型を持つシンボルの割合を数値化し、低下した場合に通知します。このツールをTypeScript言語サービスのプラグインとして設定すると、エディターで隠れたany型を可視化でき、CIシステムに組み込むことで型安全性の継続的な改善を促進できます.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment