TypeScript を使っていると、.d.ts という拡張子のファイルや、declare というキーワードを目にすることがあります。これらは「型情報だけ」を書くための仕組みで、実装(実際に動くコード)を持ちません。型が付いていない JavaScript ライブラリに型を後付けしたり、window に独自のプロパティを生やしたり、画像ファイルを import できるようにしたりと、活躍する場面は意外と多くあります。この記事では、型定義ファイル(.d.ts)とは何か、declare の基本的な書き方から、グローバル宣言・モジュール拡張・@types パッケージの入手まで、具体的なコードとあわせて解説します。
目次
型定義ファイル(.d.ts)は「型だけ」を書く場所
拡張子が .d.ts のファイルは「型宣言ファイル(declaration file)」と呼ばれ、その名のとおり型の宣言だけを書くための特別なファイルです。通常の .ts ファイルが型と実装(関数の中身や変数の値)の両方を含むのに対し、.d.ts には「こういう名前の関数や変数が、こういう型で存在する」という約束事だけを書きます。実装は書きません。
なぜこんなファイルが必要なのでしょうか。一番わかりやすいのは、型情報を持たない JavaScript のライブラリに型を付けたいときです。世の中には TypeScript が生まれる前から使われてきた JavaScript のライブラリが数多くあります。それらは .js ファイルなので型情報を持っておらず、そのまま使うと TypeScript から見ると「型が分からないもの」になってしまいます。そこで、そのライブラリがどんな関数を持っているかを別ファイルの .d.ts に書いておくことで、TypeScript が型チェックや補完をできるようになります。
もう一つは、window のようなグローバルな変数に独自のプロパティを追加したいときや、TypeScript が標準では認識しない .svg などのファイルを import できるようにしたいときです。これらはいずれも「実装は別のところにあって、TypeScript には型だけを教えたい」という状況で、まさに .d.ts の出番です。
declare で実装のない宣言を書く
.d.ts の中身を書くときに使うのが declare というキーワードです。declare は「この名前のものが(どこかに)存在する」ということを TypeScript に教えるための宣言で、実装を伴いません。たとえば、外部のスクリプトタグで読み込んだライブラリがグローバルに APP_VERSION という定数と greet という関数を用意しているとします。その型を次のように宣言できます。
// 実装は書かず、「存在すること」と「型」だけを宣言する
declare const APP_VERSION: string;
declare function greet(name: string): void;
declare class ApiClient {
constructor(baseUrl: string);
get(path: string): Promise<unknown>;
}
declare const は変数(定数)を、declare function は関数を、declare class はクラスを、それぞれ「中身なし」で宣言しています。関数には { ... } の本体を書かず、クラスにもメソッドの実装を書きません。あくまで「名前」と「型」だけを TypeScript に伝えるのが役割です。こう宣言しておくと、実際のコードから greet('Taro') や new ApiClient('https://example.com') と書いても、型チェックと補完が効くようになります。
それぞれの宣言が何を表すかを整理すると、次のようになります。
| 宣言 | 意味 |
|---|---|
declare const | 再代入されない変数(定数)の存在と型を宣言する |
declare let / declare var | 再代入されうる変数の存在と型を宣言する |
declare function | 関数の存在とシグネチャ(引数・戻り値の型)を宣言する |
declare class | クラスの存在とメンバーの型を宣言する |
declare namespace | 入れ子になった名前空間の型をまとめて宣言する |
declare global で window などのグローバルを拡張する
ブラウザで開発していると、window に独自のプロパティを持たせたい場面があります。たとえば計測タグが window.dataLayer を使っていたり、自前のフラグを window.myApp に持たせたりするケースです。そのまま window.myApp と書くと「そんなプロパティは存在しない」と型エラーになるため、グローバルの型を拡張してあげます。ここで使うのが declare global です。
export {}; // このファイルをモジュールにするための空 export
declare global {
interface Window {
// window.myApp が使えるようになる
myApp: {
version: string;
debug: boolean;
};
}
}
こう書いておくと、コード中で window.myApp.version と書いても型エラーにならず、補完も効くようになります。ポイントは interface Window を再宣言している点です。TypeScript の interface は同じ名前で複数回宣言するとマージ(合成)される性質があり、これを利用して標準の Window にプロパティを追加しています。
先頭の export {} が必要な理由
上のコードで、一見不要に見える export {}; が先頭に書かれていることに気づいたでしょうか。これは、TypeScript がファイルを「モジュール」として扱うか「スクリプト(グローバル)」として扱うかを切り替えるための記述です。import や export を一つも含まないファイルはスクリプト扱いになり、書いた宣言がそのままグローバルに公開されます。この状態だと declare global という書き方は使えず(すでにグローバルなので意味を持たない)、エラーになります。
逆に、export を一つでも含むファイルはモジュール扱いになります。declare global は「モジュールの中からグローバルスコープに手を伸ばす」ための構文なので、ファイルがモジュールである必要があります。そこで、他に export するものが何もない場合でも export {}; という空の export を書いて、ファイルを強制的にモジュールとして認識させるわけです。おまじないのように見えますが、モジュール扱いにするための明確な役割があります。
declare module で既存モジュールや非JSファイルに型を付ける
declare module を使うと、モジュール単位で型を宣言できます。よく使うのは大きく二つの場面です。一つは、型定義が用意されていないライブラリに最低限の型を与えるケース。もう一つは、.svg や .css のように TypeScript が標準では import できないファイルに型を付けるケースです。
画像などの非JSファイルを import できるようにする
webpack や Vite などのバンドラを使っていると、import logo from './logo.svg'; のように画像ファイルを import して URL を得る書き方をよく使います。ところが TypeScript はこのままだと「.svg というモジュールが分からない」とエラーを出します。バンドラ側では動くのに TypeScript が理解できていない状態です。そこで、ワイルドカード付きの declare module で .svg の型を宣言します。
// すべての .svg import を「文字列(URL)」として扱う
declare module '*.svg' {
const content: string;
export default content;
}
// .css を副作用 import(型は特に持たない)として許可する
declare module '*.css';
'*.svg' のようにアスタリスクを使うと、拡張子が .svg のすべての import にこの宣言が適用されます。ここでは content という文字列を export default しているので、import logo from './logo.svg'; と書くと logo が string 型になります。実体(実際の URL 文字列を返す処理)はバンドラが用意するので、.d.ts 側は「default export が文字列である」という型だけを教えれば十分です。
型定義のないライブラリに型を足す
型定義が同梱されておらず、後述の @types パッケージも存在しないライブラリを使うと、import した時点で型エラーになります。とりあえず動かしたいだけなら、モジュール名を指定して最小限の宣言を書けば黙らせられます。
declare module 'legacy-lib' {
// 必要な関数だけ、分かる範囲で型を書く
export function doSomething(value: string): number;
}
これで import { doSomething } from 'legacy-lib'; が型付きで使えるようになります。とにかくエラーを消したいだけなら中身を空にして declare module 'legacy-lib'; と書くこともできますが、その場合 import したものは any 扱いになり型の恩恵を受けられません。可能な範囲で使う関数の型を書いておくのがおすすめです。
型定義ファイルは @types から入手できることも多い
ここまで自分で .d.ts を書く方法を見てきましたが、有名なライブラリの型定義は、実は自分で書かなくても手に入ることがほとんどです。DefinitelyTyped というコミュニティ運営のリポジトリに、型定義を持たない多数のライブラリ向けの型が集められており、それらは @types/ライブラリ名 という npm パッケージとして公開されています。たとえば古い形式の lodash や、Node.js 自体の型は次のようにインストールできます。
# Node.js の型定義を開発依存として追加する npm install --save-dev @types/node # ライブラリの型定義を追加する例 npm install --save-dev @types/lodash
@types/* をインストールすると、TypeScript は自動的にその型を認識します。これは tsconfig.json の設定によって、既定で node_modules/@types 配下の型定義が読み込まれる仕組みになっているためです。ライブラリを使い始めて型エラーが出たら、まず @types/そのライブラリ名 が存在しないかを確認するとよいでしょう。なお最近のライブラリは型定義をパッケージ自体に同梱していることが多く、その場合は @types を入れる必要はありません。
自分で書いた .d.ts を確実に読み込ませたいときは、tsconfig.json の設定で型定義の場所を調整できます。よく使う関連設定は次のとおりです。
| 設定 | 役割 |
|---|---|
typeRoots | 型定義を探すフォルダを指定する(既定は node_modules/@types) |
types | 自動で読み込むグローバル型定義パッケージを絞り込む |
include | コンパイル対象に自作の .d.ts を含める |
多くのプロジェクトでは、include に型定義ファイルを置いたフォルダ(例: types)を含めておけば、追加設定なしで自作の .d.ts を認識してくれます。
.d.ts でつまずきやすいポイント
型定義ファイルは通常の .ts とは少し違うルールを持っているため、初めて触ると戸惑う点がいくつかあります。ここでは特に間違えやすいところを整理します。
.d.ts に実装を書いてはいけない
.d.ts はあくまで型の宣言専用のファイルです。関数の中身や変数の初期値といった実装を書くとエラーになります。たとえば declare function greet(name: string): void { console.log(name); } のように本体 { ... } を付けると「宣言ファイルに実装を含めることはできない」と怒られます。.d.ts には「存在すること」と「型」だけを書き、実装は通常の .ts や .js 側に置く、という役割分担を守りましょう。
declare した宣言は出力に残らない
declare で書いた宣言は、あくまで TypeScript のための型情報であり、コンパイル後の JavaScript には一切出力されません。declare const APP_VERSION: string; と書いても、それは「どこか別の場所で APP_VERSION が用意されている」と TypeScript に伝えているだけで、実際の値を用意してくれるわけではありません。実体を用意するのは外部スクリプトやバンドラ、あるいは自分で書く .ts 側の責任です。宣言と実装が食い違っていても TypeScript は気づけないので、宣言した型が実際のものと一致しているかは自分で保証する必要があります。
モジュール扱いとスクリプト扱いの違いに注意する
前述のとおり、import / export を含むファイルはモジュール、含まないファイルはスクリプト(グローバル)として扱われます。この違いは .d.ts の挙動を大きく変えます。グローバルな型を追加したいのにファイルがモジュールになっていると宣言が外から見えなかったり、逆にモジュールにしたつもりが export を書き忘れてグローバルを汚染してしまったりします。declare global を使うときはファイルをモジュールにする(export {}; を書く)、素朴にグローバル宣言をばらまきたいときは export を書かない、という切り替えを意識すると混乱しにくくなります。
まとめ
型定義ファイル(.d.ts)は、実装を持たず型の宣言だけを書く特別なファイルで、型のない JavaScript ライブラリに型を付けたり、グローバル変数や非JSファイルに型を与えたりするときに使います。中身は declare キーワードで書き、declare const / declare function / declare class のように「名前と型」だけを宣言します。window などのグローバルを拡張するには declare global を使い、そのときファイルをモジュール化するために export {}; を添えます。declare module '*.svg' のようなワイルドカード宣言で画像などの import に型を付けられ、既存ライブラリの型は declare module で補えます。多くの有名ライブラリの型は @types/*(DefinitelyTyped)から入手できるので、まずはそちらを探すのが近道です。.d.ts に実装を書かないこと、declare は出力に残らないこと、モジュール扱いとスクリプト扱いの違いを押さえておけば、型定義ファイルはぐっと扱いやすくなります。