新しいブラウザAPIで発生するTypeScriptエラーの解決方法
modern-web-typesや独自の宣言で、新しいブラウザーAPIのTypeScriptエラーを修正。DOM型が遅れる理由と実行時の対応確認方法を解説します。
一部の新しいブラウザAPIを使うと、TypeScriptは「Property does not exist on type」というエラーを報告します。これは、TypeScriptに組み込まれたDOM型には、少なくとも2つのブラウザエンジンがサポートしているAPIしか含まれていないためです。このエラーは、より網羅的なDOMライブラリに差し替えるか、不足しているメンバーを自分で宣言することで解決できます。
MDNにドキュメントがあり、Chromeでもすでに動作するAPIを呼び出したのに、コンパイラは「存在しない」と言ってきます。anyとして型付けすれば赤い波線は消えますが、そのコードの型チェックは失われてしまいます。
本記事では、TypeScriptのDOM型がブラウザの実装に遅れをとる理由を説明したうえで、型安全な2つの解決策と、どちらの方法でも必要となるランタイムチェックについて解説します。
重要なポイント
- TypeScriptのlib.dom.d.tsはmicrosoft/TypeScript-DOM-lib-generatorによって生成されており、主要ブラウザエンジンのうち2つ未満でしかサポートされていないAPIは除外されています。
- modern-web-typesは、この基準を「1つの安定版エンジン」まで引き下げています。
npm install --save-dev @typescript/lib-dom@npm:modern-web-typesでインストールできます。 - TypeScript 6以降では、compilerOptionsで
"libReplacement": trueを指定しない限り、このパッケージは有効になりません。TypeScript 4.5〜5.xでは追加の設定は不要です。 - 不足しているメンバーを1つだけ自分で追加する場合は、グローバルな
.d.tsファイルでinterface Windowを使用します。typeエイリアスではマージされません。 - APIの型が存在することは、ブラウザがそのAPIをサポートしていることを意味しません。そのため、単一エンジンでのみ実装されているAPIには、引き続き
'fetchLater' in windowのようなチェックが必要です。
「Property Does Not Exist on Type」とは何を意味するのか
TypeScriptのエラーTS2339「Property does not exist on type」は、TypeScriptに同梱されているDOM型で宣言されていない、実在するブラウザAPIをコードから呼び出したときに発生します。自分のコードでwindow.myThingのようなグローバルを追加したわけではありません。このメソッドはブラウザが提供しているものですが、TypeScriptの型がそれを認識していないのです。
window.fetchLater('/analytics', { method: 'POST', activateAfter: 60_000 });
// error TS2339: Property 'fetchLater' does not exist on type 'Window & typeof globalThis'.
fetchLater()は、ユーザーがページを離れたとき(ページがアンロードされるか、bfcacheに入ったとき)、またはオプションのactivateAfterタイムアウトが先に切れた場合はその時点で、ブラウザが送信するリクエストをキューに入れます。Philip Walton氏によるmodern-web-typesの発表記事でも、同じエラーが発生するAPIの例として、要素スコープのView TransitionsやLong Animation Frameのscriptsプロパティが挙げられています。
TypeScriptのDOM型がブラウザに遅れをとる理由
TypeScriptのDOM型がブラウザに遅れをとるのは意図的なものです。TypeScript-DOM-lib-generatorのREADMEでは、主要エンジンであるGecko(Firefox)、Blink(Chrome/Edge)、WebKit(Safari)のうち2つ以上での実装を収録の基準としており、ベンダー間で実際に合意が得られたAPIのみが含まれるようになっています。Chromium系のブラウザはすべてBlinkで動作するため、まとめて1つのエンジンとして数えられます。
これが実際に意味するところは次のとおりです。Blinkでのみ提供されているAPIは、Chrome、Edge、その他すべてのChromium系ブラウザで動作しますが、TypeScriptはそれでも「存在しない」と判断します。web-featuresのfetchLaterのエントリでは、サポートしているブラウザとしてChromium系ブラウザのみが記載されているため、2エンジンルールを満たしていません。
「Property does not exist on type」エラーは、TypeScriptの収録ポリシーを反映したものです。コードを実行しているブラウザがそのAPIをサポートしているかどうかを示すものではありません。
解決策1:lib.domをmodern-web-typesに置き換える
modern-web-typesはPhilip Walton氏が開発したパッケージで、コードに他の変更を加えることなく、TypeScriptのDOMライブラリとWebWorkerライブラリを置き換えます。氏の発表記事で説明されているとおり、TypeScript自身のジェネレーターとw3c/webrefのデータを使って構築されていますが、1つの安定版ブラウザエンジンで提供されたAPIであれば収録されます。
@typescript/lib-domというエイリアスでインストールします。これは、TypeScriptがDOMライブラリを解決する際に探す名前です。
npm install --save-dev @typescript/lib-dom@npm:modern-web-types
TypeScript 6以降では、エイリアスをインストールしただけでは何も起こらず、lib replacementを明示的に有効化する必要があります。
{
"compilerOptions": {
"libReplacement": true
}
}
TypeScript 4.5〜5.xでは、この設定は不要です。TypeScript 4.5で@typescript/lib-*の参照機能が導入され、自動的に適用されていました。TypeScript 5.8でこの機能はlibReplacementフラグの管理下に置かれましたが、デフォルトでは有効のままでした。そしてTypeScript 6でデフォルトが無効に変更されました。この経緯は、lib replacementの使用を推奨するmodern-web-typesのプルリクエストで説明されています。TypeScript 7.0の発表によると、7.0はlibReplacement: falseを含む6.0のデフォルト設定を引き継いでいるため、7.0でもこのフラグが必要です。
Workerプロジェクトの場合
Workerのコードには、typesで追加する別のエントリポイントを使用します。このエントリポイントが"WebWorker"を置き換えるため、libには"WebWorker"を含めないでください。
{
"compilerOptions": {
"types": ["modern-web-types/webworker"],
"lib": ["ESNext"]
}
}
TypeScript 5.xでは、typesを指定すると、node_modules内のすべての@types/*パッケージが自動的に読み込まれなくなります。TypeScript 6以降では、typesのデフォルトがすでに[]になっているため、いずれにしても自動読み込みは行われません。どちらの場合も、@types/nodeなどのパッケージに依存している場合は、同じ配列に追加してください。READMEには、serviceworker、sharedworker、audioworkletのエントリポイントも記載されています。
解決策2:インターフェースのマージで不足しているAPIを宣言する
必要なAPIが1つだけであれば、小さな宣言ファイルで十分です。組み込みのインターフェースと同じ名前でインターフェースを宣言すると、TypeScriptが宣言をマージします。このとき、必ずinterfaceを使用する必要があります。type Window = { ... }のようなエイリアスは、型エイリアスがマージされないため、識別子の重複エラーになります。
以下のシグネチャはMDNに準拠しています。fetchLater()はfetch()と同じ引数に加えてactivateAfterオプションを受け取り、FetchLaterResultを返します。FetchLaterResultのactivated(boolean)で、リクエストがすでに送信されたかどうかを確認できます。
// src/types/fetch-later.d.ts
interface DeferredRequestInit extends RequestInit {
activateAfter?: number;
}
interface FetchLaterResult {
readonly activated: boolean;
}
interface Window {
fetchLater(input: RequestInfo | URL, init?: DeferredRequestInit): FetchLaterResult;
}
トップレベルにimportやexportを含まない.d.tsファイルはグローバルスクリプトとして扱われるため、declare globalで囲まなくても機能します。正しく動作させるためのルールは次の3つです。
- ファイルをモジュールにしない。 トップレベルに
importやexportを追加すると、そのファイルはモジュールになります。インターフェースがグローバルにマージされなくなり、再びdeclare globalが必要になります。 - tsconfigの対象に含める。 ファイルが
includeのパターンに一致している必要があります。 - TypeScriptがAPIを正式に収録したら削除する。 lib.domが同じメンバーを宣言するようになると、自前の宣言は良くても冗長、悪ければ競合の原因になります。
この宣言で型付けされるのはwindow.fetchLater(...)のみです。グローバル関数としてfetchLater(...)を直接呼び出すには、別途グローバル関数の宣言が必要になるため、コードではwindow.fetchLaterを使用するようにしてください。
機能検出は引き続き必要か
必要です。TypeScriptの型が表すのはAPIの形であり、どのブラウザがそのAPIを備えているかではありません。どちらの解決策でもコンパイラはfetchLaterを受け入れるようになりますが、このAPIを持たないブラウザでは実行時にエラーがスローされます。そのため、呼び出しをガードしてください。
if ('fetchLater' in window) {
try {
window.fetchLater('/analytics', { method: 'POST', activateAfter: 60_000 });
} catch {
// quota exceeded: fall back or drop the beacon
}
} else {
// fall back, e.g. send the data immediately
}
tryブロックを使っているのは、MDNがほぼすべてのfetchLater()の呼び出しでQuotaExceededErrorをキャッチすることを推奨しているためです。単一エンジンでのみ実装されているAPIはプログレッシブエンハンスメントとして扱い、MDNのfetchLaterのページで、自身のターゲットブラウザにおけるサポート状況を確認してください。
どちらの解決策を選ぶべきか
| アプローチ | セットアップ | カバー範囲 | TypeScriptが対応した後 |
|---|---|---|---|
| modern-web-types | インストール1回、TS 6以降はフラグ1つ | 単一エンジンで実装されたすべてのAPI | 削除するものはなし |
手書きの.d.ts | APIごとに小さなファイル1つ | 自分で書いたものだけ | ファイルを削除 |
as any / @ts-ignore | なし | なし | 実際のエラーを無期限に隠してしまう |
プロジェクトで複数の新しいAPIを使用している場合や、他のエンジンに実装される前の機能を日常的に採用している場合は、modern-web-typesを使いましょう。必要なメンバーが1つか2つで、DOMライブラリ全体を置き換えたくない場合は、手書きの.d.tsを使いましょう。as anyや@ts-ignoreは避けてください。これらは式全体や行全体のチェックを無効化し、後々本当のミスを隠してしまいます。TypeScript ESLintのセットアップで@typescript-eslint/no-explicit-anyと@typescript-eslint/ban-ts-commentのルールを適用すれば、場当たり的な回避策が積み重なるのを防げます。
まとめ
新しいブラウザAPIで発生するTypeScriptの「Property does not exist on type」エラーは、TypeScriptの2エンジンルールに起因するものであり、コードの誤りや"dom"の指定漏れが原因ではありません。新しいAPIを日常的に使うならlib.domをmodern-web-typesに置き換え、単発で必要なだけならWindowにインターフェースを1つマージしましょう。そして、すべての呼び出しをランタイムチェックで囲んでください。まずはTypeScriptのバージョンを確認しましょう。6以降であれば、パッケージのインストール時に"libReplacement": trueを追加してください。
よくある質問
@types/webをインストールすれば、新しいブラウザAPIの型不足は解消されますか?
すでに2エンジンルールを満たしているAPIに限り解消されます。@types/webはTypeScriptに同梱されているものと同じDOMライブラリです。microsoft/TypeScript-DOM-lib-generatorによって生成されてnpmに公開されているため、TypeScriptの次のリリースより先に新しいDOM型を提供できます。ただし、同じ収録ポリシーが適用されるため、fetchLaterのようなChromium専用のAPIは引き続き含まれません。TypeScript 4.5以降では同じ@typescript/lib-domエイリアスでインストールでき、TypeScript 6以降ではlibReplacementをtrueに設定する必要もあります。
libオプションにDOMやESNextを追加すればエラーは解消されますか?
いいえ。libオプションは、TypeScriptに同梱されている宣言ファイルのうちどれを読み込むかを選択するだけです。同梱のDOMファイルは2エンジンルールに基づいて生成されているため、'DOM'、'DOM.Iterable'、'ESNext'を追加しても、そのファイルに含まれていないAPIが追加されることはありません。ESNextが追加するのはJavaScriptの言語機能であり、ブラウザAPIではありません。エラーを解消するには、DOMライブラリをmodern-web-typesに置き換えるか、グローバルな.d.tsファイルで不足しているメンバーを宣言してください。
WindowではなくElementやHTMLElementに不足しているメソッドを宣言するにはどうすればよいですか?
仕様でそのメンバーが定義されているインターフェースにマージします。トップレベルにimportやexportを含まないグローバルな.d.tsファイルで、新しいメソッドを持つinterface Elementを宣言してください。HTMLElement、SVGElement、その他の要素インターフェースはElementを継承しているため、すべてがそのメソッドを引き継ぎます。要素スコープのstartViewTransition()はElementに定義されているため、HTMLElementにのみ宣言するとSVG要素が型付けされないままになります。正確なシグネチャはMDNまたは仕様から確認してください。
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.
Star on GitHub12k