-
-
Save ryo-murai/b06e1340bbef89dc3cda88b169ddd295 to your computer and use it in GitHub Desktop.
新しいTypeScript開発者は、JavaScriptからの移行時に、すべての変数に型アノテーションを記述しがちです。しかし、TypeScriptの強力な型推論機能により、多くの型アノテーションは不要であり、コードのノイズを増やし、かえって型安全性を低下させる可能性さえあります。TypeScriptは、number
のようなプリミティブ型だけでなく、より複雑なオブジェクトや配列の型も正確に推論できます。明示的な型アノテーションを省略することで、より正確なリテラル型(例: string
ではなく"y"
)が推論されることもあり、これが型安全性を向上させます。また、型推論に頼ることで、分割代入などのリファクタリングが容易になります。
型アノテーションが依然として必要な主な状況は以下の通りです:
- 関数のパラメーター: TypeScriptは通常、変数の型を最初に登場したときに決定するため、関数のパラメーターには明示的な型アノテーションが必要となることが多いです(デフォルト値がある場合や、型宣言を持つライブラリのコールバックとして使用される場合は推論されることもあります).
- 関数の戻り値の型: デフォルトでは不要とされていますが、複数の
return
文がある場合、パブリックAPIの一部である場合、または戻り値で名前付きの型を使用したい場合には、実装上のエラーが呼び出し元のコードに漏れ出すのを防ぎ、関数の契約を明確にするために、戻り値の型アノテーションを追加することが推奨されます. - オブジェクトリテラル: オブジェクトリテラルに明示的な型アノテーションを付けることで、余剰プロパティチェックが有効になり、エラーが宣言箇所近くで報告されるようになります.
typescript-eslint
のno-inferrable-types
ルールを使用すると、推論可能なプリミティブ型に明示的な型アノテーションを付けることを禁止できます.
JavaScriptでは、1つの変数を異なる型の値に再利用することが可能ですが、TypeScriptでこれをモデリングするのは難しく、予期せぬ挙動につながることがあります。そのため、意味的に異なる値には新しい変数を導入することが推奨され、型の衝突や混乱を避けるように促しています。
TypeScriptは、リテラル値からより広い型(例: "x"
からstring
、12
からnumber
)を推論するプロセスを「型拡大(widening)」と呼びます。これは、TypeScriptが型の具体性と柔軟性のバランスを取ろうとする挙動です。
この型拡大の挙動を制御する方法はいくつかあります:
const
宣言:let
の代わりにconst
で変数を宣言すると、より狭い型(例:"x"
から"x"
)が推論されます.- 明示的な型アノテーション: 型アノテーションを追加することで、推論された型を上書きできます.
- 文脈の利用: 値を関数の引数として渡すなど、型チェッカーに追加の文脈を与えることで、より正確な型が推論されることがあります(詳細は項目24で説明).
as const
アサーション: 値の型を可能な限り最も狭いリテラル型として推論させる「const文脈」を適用します。これは型レベルの構文であり、実行時の値には影響しません.satisfies
演算子:as const
と同様に型を狭くしますが、値が特定の型を満たしているかをチェックしつつ、推論された型はそのリテラル型を保持します。これにより、エラーが値の定義箇所で報告されるようになります.
TypeScriptでは変数の型は通常変化しないため、JavaScriptでオブジェクトを段階的に構築するパターンは、TypeScriptでは問題を引き起こしやすいです。初期値が空のオブジェクト({}
)の場合、TypeScriptはそれを空の型と推論するため、後からプロパティを追加しようとするとエラーになります。
この問題を解決するには、オブジェクトを一度に構築する(すべてのプロパティを最初のリテラルで指定する)か、明示的な型アノテーションを使用して、TypeScriptが空のオブジェクトからより広い型を推論しないようにする必要があります.
TypeScriptでは、変数の型はコードの場所に応じて変化することがあり、このプロセスは「制御フロー解析」として知られています. TypeScriptは、コードの実行フローに基づいて変数の型をより具体的に「絞り込む(narrowing)」ことができます。
型の絞り込みをトリガーする一般的なJavaScriptの構文には以下のものがあります:
if
文(!elem
のようなnullチェック、typeof
による型チェック、instanceof
、in
演算子によるプロパティの有無チェック)switch
文(特にタグ付きユニオンと相性が良い)Array.isArray
などの標準関数- ユーザー定義の型ガード(User-defined type guards):
value is Type
という戻り値の型述語を持つ関数を定義することで、カスタムの絞り込みロジックを実装できます. - TypeScript 5.5以降では、特定の条件を満たす関数において、関数の型述語が自動的に推論されるようになりました.
ただし、プロパティの型絞り込みは、関数呼び出しによって無効にされないという「実用的な選択」がTypeScriptによってなされており、これが不健全性につながる可能性もあります(詳細は項目48で説明).
ネストしたプロパティをローカル変数で扱うと、エイリアスを作成したことになる
エイリアスの型絞り込みはオリジナルのプロパティには反映されないし、その逆も然り。だから関数内で併用しないこと
プロパティを使う場合は型チェック後に変わるリスクがある(けど TypeScriptは実用的な選択によりスルーしている) この問題を避けるには readonlyによって変更不可とする
TypeScriptの型推論は、値が使われる**文脈(コンテキスト)**に大きく依存します。これは「文脈的型付け(contextual typing)」と呼ばれます。TypeScriptは、変数が最初に導入されたタイミングでその型を決定するのが一般的ですが、その後の使用文脈から型を推論することもあります。
文脈が失われると型エラーにつながることがあります。一般的な例は以下の通りです:
- 文字列リテラル型:
let
で宣言された変数が、後から特定の文字列リテラル型を期待する関数に渡される場合、let
変数にはより広いstring
型が推論されてしまい、エラーになることがあります. - タプル型: タプル型を期待する関数に配列リテラルを渡す際に、中間変数に代入すると、タプル型ではなくより広い配列型が推論されてしまうことがあります.
- オブジェクトリテラル: 特定の文字列リテラル型を含むプロパティを持つオブジェクト型を期待する関数に、オブジェクトリテラルを渡す際に、中間変数に代入すると、プロパティがより広い
string
型に拡大されてしまい、エラーになることがあります. - コールバック関数: コールバック関数を他の関数にインラインで渡す場合、TypeScriptは文脈からコールバックのパラメーターの型を推論します.
これらの問題は、型アノテーションの追加や、**const
アサーション(as const
)**を使用して型を狭く保つことで解決できます. また、エラーを避けるために、インラインで値を使用する(中間変数に代入しない)形式が好ましい場合もあります.
TypeScriptでは通常、変数の型は宣言時に決定され、その後は型を絞り込むことしかできません。しかし、null
、undefined
、または空の配列[]
で初期化された変数は、後から要素が追加されることで型が「進化」する(より広くなる)という注目すべき例外があります。
例として、空の配列[]
で初期化された変数は、最初はany[]
型と推論されますが、number
型の値がpush
されるとnumber[]
型へと進化します。これは絞り込みとは異なり、型のドメインが拡大されるプロセスです。この挙動を理解することで、必要な型アノテーションを減らすことができます。
ただし、進化する型は、意図しない型エラーを見逃す原因にもなりうるため、必要に応じて明示的な型アノテーションを追加することで、より厳密なチェックを行うことも検討すべきです。
JavaScriptの標準的な関数型API(例: map
、flatMap
、filter
、reduce
)や、Lodashのようなユーティリティライブラリの関数を使用することは、手書きのループよりも推奨されます。これは、これらの関数に付属するTypeScriptの型宣言が、型をコード全体に流すのを助けるためです。手書きのループでは、プログラマー自身が型を管理する必要があるのに対し、APIやライブラリの型宣言は、型推論を効率的に機能させ、コードの正確性を向上させます。
JavaScriptで非同期処理を扱う際、PromiseベースのAPIよりもasync/await
構文を使用することが推奨されます。主な理由は以下の2点です:
- コードがより簡潔で分かりやすくなる。
async
関数は常にPromise
を返すことが強制されるため、型推論が簡素化され、潜在的なバグを回避できます. たとえawait
するものがなくても、async
関数はPromise
型を返します.
これにより、TypeScriptの型推論の仕組みが最大限に活用され、コールバックベースのAPIで発生しがちな型関連の混乱を防ぐことができます.
複数のジェネリック型パラメーターを持つ関数において、TypeScriptの型推論は「全か無か」の性質を持ちます。つまり、すべての型パラメーターを推論させるか、またはすべてを明示的に指定する必要があります。
型パラメーターを部分的に推論させ、よりきめ細かく制御したい場合には、以下の2つの選択肢があります:
- クラス: ジェネリッククラスを使用することで、クラスがインスタンス化されるときに型パラメーターが設定され、その後のメソッド呼び出しで再度の型指定は不要になります.
- カリー化: 関数をカリー化することで、複数のジェネリック型パラメーターを段階的に割り当てることが可能になります. カリー化のアプローチは、ローカルの型エイリアスを定義できるスコープを作成するという利点があり、複雑な型表現の重複を減らすことができます.
どちらを選択するかはAPIの設計と好みに依存しますが、TypeScriptの文脈ではカリー化がローカルの型エイリアスを定義できる点で有利な場合があります.
『Effective TypeScript 第2版』の5章「不健全性とany型」では、TypeScriptの型システムが持つ漸進的かつオプション的な性質に焦点を当て、any
型を賢く使用してそのデメリットを抑える方法、そしてシンボルの静的な型と実行時の型に互換性がない「不健全性」という、より一般的な問題とその回避策について説明しています。
以下に、5章の各項目を要約します。
-
項目43 可能なかぎり狭いスコープでany型を使う
any
型は型チェックを効果的に無効化する強力なツールですが、その使用は慎重に行う必要があります。エラーが発生した際に、any
型のスコープをできるだけ狭くすることが推奨されます。これは、@ts-expect-error
や@ts-ignore
といったコメントを使って特定の行のエラーを抑制したり、オブジェクト全体にas any
を適用するのではなく、問題のあるプロパティに限定してas any
を使用したりすることで実現できます。any
を返すと、その関数を呼び出すコードで型安全性が失われるため、関数からany
型を返すことは避けるべきです。typescript-eslint
のno-unsafe-assignment
やno-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
型の使用、オブジェクトや配列へのチェックなしでのアクセス、不正確な型定義、クラス階層におけるメソッドの双変性、オブジェクトや配列の変性の不正確なモデリング、関数のパラメーターの変更、オプションプロパティなどがあります。strictNullChecks
やnoUncheckedIndexedAccess
といったコンパイラーオプションを有効にすることで、厳密さを調整し、不健全性の原因となるパターンを認識して回避することが重要です. -
項目49 型カバレッジを監視し、型安全性のリグレッションを防ぐ
noImplicitAny
を有効にしても、明示的なany
型やサードパーティの型宣言(@types
)を通じて、any
型がコードに入り込む可能性があります。型安全性や開発者体験を損なうany
型の問題を監視するために、type-coverage
のようなツールを使用することが推奨されます。type-coverage
は、プロジェクト内のany
型以外の型を持つシンボルの割合を数値化し、低下した場合に通知します。このツールをTypeScript言語サービスのプラグインとして設定すると、エディターで隠れたany
型を可視化でき、CIシステムに組み込むことで型安全性の継続的な改善を促進できます.