Menu

TypeScriptのdeclareと型定義ファイル(.d.ts)を解説

.d.ts ファイルは、JavaScriptのコードを含まずにその型だけを記述します。declare キーワードは .ts ファイルの中で同じ役割をします。型定義ファイルの生成方法、@types パッケージの位置づけ、型のないモジュールへの型付け、declare global とモジュール拡張で既存の型を広げる方法を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

型定義ファイル(.d.ts)には型しか含まれません。どこか別の場所にあるJavaScriptのシグネチャ、インターフェース、クラスの形です。declare キーワードは、普通のファイルの中で同じ役割をします。コンパイラーに「これは実行時に存在するので信じてほしい」と伝え、コードは出力しません。

宣言が文字列だと言っているので、コンパイラーは __APP_VERSION__.length を受け入れました。実行時にはそんな変数は存在しないので、.length を読むと ReferenceError: __APP_VERSION__ is not defined が投げられます。コンパイラーがまったく予期していなかった実行時の例外です。これがあらゆる宣言の約束事です。型が正しいかどうかは、その裏にあるJavaScript次第です。

.d.ts ファイルに書くもの

declaration: true でコンパイルしてみるのがいちばんわかりやすいでしょう。次のソースから:

// price.ts
export interface LineItem {
    name: string;
    price: number;
    qty: number;
}

const TAX = 0.2;

export function total(items: LineItem[]) {
    const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
    return Math.round(sum * (1 + TAX) * 100) / 100;
}

export class Cart {
    private items: LineItem[] = [];
    add(item: LineItem) {
        this.items.push(item);
        return this;
    }
}

tsc は price.js と次の price.d.ts を書き出します:

export interface LineItem {
    name: string;
    price: number;
    qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
    private items;
    add(item: LineItem): this;
}

関数の本体はなくなり、推論された戻り値の型(number、this)が書き出され、private フィールドは名前だけが残って型は失われ、エクスポートしていない TAX はまったく含まれません。ライブラリは、Node 用に .js を、エディターとコンパイラー用に .d.ts を公開します。バンドラーでJavaScriptをビルドするプロジェクトでは、emitDeclarationOnly: true で .d.ts ファイルだけを出力します。

毎日使っている ES2022 の組み込み機能も型定義ファイルから来ています。lib.es2022.d.ts などはTypeScriptに同梱されていて、target と lib で選ばれます。

declare の書き方

declare 文はすべて、すでに存在するものを記述します。import も export もない .d.ts ファイル(グローバルな型定義ファイル)では、次のそれぞれがプロジェクト全体から見えるようになります:

// globals.d.ts
declare const API_URL: string;                     // a global constant
declare let debugMode: boolean;                    // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget {                             // a class from a script tag
    constructor(el: string);
    render(): void;
}
declare namespace Analytics {                      // a global object
    function page(name: string): void;
}
declare module "legacy-charts" {                   // a module you import
    export function draw(data: number[]): void;
}

.d.ts ファイルの中では、interface と type に declare は要りません。それ以外のトップレベルの宣言で declare も export もないものはエラー TS1046 になります。ファイルに import か export があると、その宣言はそのファイルのローカルになり、グローバルスコープへの追加は(下で紹介する)declare global ブロックに書きます。コンパイラーは、宣言されたシグネチャに照らして呼び出しをチェックします:

コンパイラーは index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. と報告します。{ items: "3" } を渡すか、実際の関数が数値を受け付けるなら宣言を変えます。

@types パッケージ

多くの npm パッケージは、package.json の types フィールド(または exports の types 条件)から参照される独自の .d.ts ファイルを同梱しています。JavaScriptだけのパッケージには、コミュニティが管理する DefinitelyTyped プロジェクトが @types スコープで型を公開しています:

npm i lodash
npm i -D @types/lodash

パッケージを import すると、TypeScriptは自動的に node_modules/@types/{name} を探します。import ではなくグローバルを記述する型、たとえば @types/node(process、Buffer)やテストランナーの describe と it は、tsconfig.json に列挙する必要があります:

{
    "compilerOptions": {
        "types": ["node"]
    }
}

このエントリーがないと、TypeScript 7 はパッケージがインストールされていても型を読み込みません:

error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

tsc --init は新しい設定に "types": [] を書き込み、Node のプロジェクトには ["node"] を勧めるコメントを付けます。

型のないモジュールに型を付ける

同梱にも @types にも型がないJavaScriptのパッケージを import すると、エラー TS7016 になります:

error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.

プロジェクトのどこか(types/ フォルダーがよく使われます。include の対象であればどこでもかまいません)に、トップレベルに import も export もない .d.ts ファイルを置いて直します。使う部分を記述しましょう:

// types/fakelib.d.ts
declare module "fakelib" {
    export function hi(name: string): string;
    export const version: string;
}

// Non-code files a bundler lets you import
declare module "*.svg" {
    const url: string;
    export default url;
}

いちばん短い書き方の declare module "fakelib"; の1行だけでは、そのパッケージからの import がすべて any になります。エラーは消えますが、チェックもすべて消えるので、一時的な手段として扱ってください。

declare global

ポリフィル、組み込みの新しいメソッド、script タグで設定されるグローバル変数など、グローバルスコープに追加するコードには、対応する型が必要です。それを追加するのが declare global です。モジュール(import か export のあるファイル)の中に置く必要があるので、先頭に export {} があります:

interface Array<T> は組み込みの Array インターフェースを置き換えるのではなく、それとマージされます。また、globalThis にプロパティを追加するのは var です(let や const ではありません)。共有するコードで組み込みのプロトタイプを拡張するのは危険です。同じ declare global の手法で、プロジェクトは @types/node の NodeJS.ProcessEnv インターフェースを通じて process.env に独自の変数を追加しています。

モジュール拡張

import するパッケージの型に追加するには、そのモジュール名を宣言し直してインターフェースを開き直します。ファイル自体がモジュールである必要があります(import がその役割を果たし、export {} でもかまいません)。import も export もないと、同じブロックがまったく新しい config-lib モジュールを宣言してしまい、パッケージの本来の型が隠れます:

// types/config-lib.d.ts
import "config-lib";

declare module "config-lib" {
    interface Settings {
        beta: boolean; // merged into the package's own Settings interface
    }
}

これで、config-lib の load().beta はどこでも boolean 型になります。インターフェースはマージされますが、型エイリアスはマージされないので、これが働くにはライブラリが interface をエクスポートしている必要があります。プラグインがフレームワークのリクエストや設定オブジェクトにフィールドを追加するのはこの方法です。

skipLibCheck

skipLibCheck: true にすると、コンパイラーは node_modules の中のものも含めて .d.ts ファイルの型チェックをしなくなります。自分のコードは引き続きそれらに照らしてチェックされます。時間を節約でき、宣言が食い違う2つのパッケージによるエラーを避けられるので、tsc --init はこれを有効にします。その代わり、自分の .d.ts ファイルの中の間違いも報告されなくなります。

よくある質問

TypeScriptの .d.ts ファイルとは何ですか?

型定義ファイル(宣言ファイル)です。型(関数のシグネチャ、インターフェース、クラスの形)だけを持ち、実装はありません。コンパイル済みのライブラリやブラウザーの組み込み API のように、どこか別の場所にあるJavaScriptを記述し、それを使うコードをTypeScriptがチェックできるようにします。tsc はこのファイルからJavaScriptを出力しません。

TypeScriptの declare キーワードは何をしますか?

値を作らずに、その値が実行時に存在することをコンパイラーに伝えます。declare const VERSION: string; と書くと VERSION を文字列として使えるようになり、この行は出力から消えます。実際に VERSION を定義するものがなければ、プログラムは実行時に ReferenceError で失敗します。

「Could not find a declaration file for module」を直すには?

エラー TS7016 は、パッケージに型がないことを意味します。型があればインストールし(npm i -D @types/package-name)、なければ使う部分を declare module "package-name" { ... } で記述した .d.ts ファイルを追加します。declare module "package-name"; の1行だけでもモジュール全体を any として型付けしてエラーを消せます。

TypeScriptから .d.ts ファイルを生成するには?

tsconfig.json で "declaration": true を設定します(または --declaration を渡します)。すると各 .ts ファイルから .js の隣に .d.ts が作られます。JavaScriptをほかのツールでビルドする場合は emitDeclarationOnly を、エディターで型からソースへジャンプできるようにするには declarationMap を追加します。

declare global と declare module の違いは何ですか?

declare global { ... } はグローバルスコープに追加します。たとえば Array の新しいプロパティやグローバル変数です。declare module "name" { ... } は、その名前で import するモジュールを記述します。declare global はモジュール(import か export のあるファイル)の中に置く必要があります。declare module は、import も export もないファイルでは新しいモジュールを宣言し、モジュールの中では既存のモジュールを拡張します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める