TypeScript型定義ファイルとは

TypeScriptで開発を進めていると、JavaScriptで書かれたサードパーティライブラリを利用する機会が多くあります。しかし、JavaScriptには型情報が含まれていないため、そのままではTypeScriptの強力な型チェック機能の恩恵を受けられません。

この問題を解決するのが「型定義ファイル(.d.ts)」です。型定義ファイルは、JavaScriptコードの型情報のみを記述した特殊なファイルで、TypeScriptコンパイラに対して「このライブラリにはこのような型が存在する」と教える役割を果たします。

前提条件

この記事を読む上での前提条件は以下の通りです。

  • Node.js 18以上がインストールされていること
  • TypeScript 5.0以上がインストールされていること
  • TypeScriptの基本的な型(interface、type、ジェネリクス)を理解していること
  • npmまたはyarnの基本的な使い方を理解していること

型定義ファイルの基本構造

型定義ファイル(.d.ts)は、実行可能なコードを含まず、型情報のみを記述します。以下は基本的な構造の例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// example.d.ts
declare function greet(name: string): string;

declare interface User {
  id: number;
  name: string;
  email: string;
}

declare class Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

このファイルには実装コードが含まれていません。declareキーワードを使って「このような関数・インターフェース・クラスが存在する」と宣言しているだけです。

TypeScriptにおける型定義ファイルの種類

TypeScriptで扱う型定義ファイルは、主に3つのカテゴリに分類されます。

組み込み型定義

TypeScriptには、JavaScript標準のAPIに対する型定義が最初から含まれています。ArrayStringMathPromiseなどの標準オブジェクトや、DOM APIの型定義がこれに該当します。

1
2
3
4
5
// TypeScriptは標準のMathオブジェクトの型を認識している
const max = Math.max(5, 10); // number型と推論される

// DOMの型も組み込みで提供される
const element = document.getElementById("app"); // HTMLElement | null

これらの型定義はlib.*.d.tsという形式のファイルで提供されており、tsconfig.jsonlibオプションで利用する定義を選択できます。

バンドルされた型定義

最近のnpmパッケージの多くは、パッケージ自体に型定義ファイルを同梱しています。この場合、パッケージをインストールするだけで型定義も自動的に利用可能になります。

1
npm install axios

axiosのように型定義がバンドルされているパッケージでは、追加の設定なしで型サポートを受けられます。パッケージに型定義が含まれているかは、package.jsontypesまたはtypingsフィールドで確認できます。

1
2
3
4
{
  "name": "axios",
  "types": "index.d.ts"
}

@typesパッケージ(DefinitelyTyped)

型定義がバンドルされていないパッケージには、DefinitelyTypedリポジトリで管理されている型定義を@typesスコープのパッケージとしてインストールできます。

1
2
3
4
5
# lodashの型定義をインストール
npm install --save-dev @types/lodash

# Reactの型定義をインストール
npm install --save-dev @types/react @types/react-dom

DefinitelyTypedと@typesパッケージの活用

DefinitelyTypedは、TypeScriptコミュニティによって運営される世界最大の型定義リポジトリです。数千ものライブラリに対する型定義が登録されており、@typesスコープのnpmパッケージとして自動的に公開されます。

@typesパッケージのインストール方法

@typesパッケージの命名規則は非常にシンプルです。元のパッケージ名の前に@types/を付けるだけです。

1
2
3
4
5
6
7
# 基本的なインストールコマンド
npm install --save-dev @types/パッケージ名

# 具体例
npm install --save-dev @types/node
npm install --save-dev @types/express
npm install --save-dev @types/lodash

スコープ付きパッケージ(@scope/package形式)の場合は、スラッシュをアンダースコア2つに置き換えます。

1
2
# @babel/coreの型定義
npm install --save-dev @types/babel__core

型定義のバージョン管理

@typesパッケージのバージョンは、対応するライブラリのバージョンと同期しています。メジャーバージョンとマイナーバージョンは元のライブラリに合わせ、パッチバージョンは型定義独自の更新に使用されます。

1
2
3
4
5
6
7
8
{
  "dependencies": {
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "@types/lodash": "^4.17.0"
  }
}

TypeScriptによる@typesの自動認識

TypeScriptは、node_modules/@typesディレクトリ内の型定義を自動的に認識します。この動作はtsconfig.jsontypeRootsおよびtypesオプションで制御できます。

1
2
3
4
5
6
{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"],
    "types": ["node", "jest"]
  }
}

typeRootsを指定すると、そのディレクトリ内の型定義のみが検索対象になります。typesを指定すると、指定したパッケージの型定義のみが自動的に含まれるようになります。

declare文の使い方

declareキーワードは、TypeScriptに対して「この要素はどこか別の場所で定義されている」と伝えるために使用します。型定義ファイルでは必須の構文です。

declare variable - 変数の宣言

グローバル変数やスクリプトタグで読み込まれた外部ライブラリの変数を宣言できます。

1
2
3
4
5
6
// グローバル変数の宣言
declare const VERSION: string;
declare let DEBUG_MODE: boolean;

// 使用例
console.log(VERSION); // 型エラーなしで使用可能

declare function - 関数の宣言

グローバルスコープに存在する関数を宣言します。オーバーロードにも対応しています。

1
2
3
4
5
6
// 単純な関数宣言
declare function formatDate(date: Date): string;

// オーバーロードを持つ関数
declare function ajax(url: string): Promise<unknown>;
declare function ajax(url: string, options: RequestOptions): Promise<unknown>;

declare class - クラスの宣言

外部ライブラリで定義されたクラスの型を宣言します。

1
2
3
4
5
declare class EventEmitter {
  on(event: string, listener: (...args: unknown[]) => void): this;
  emit(event: string, ...args: unknown[]): boolean;
  removeListener(event: string, listener: (...args: unknown[]) => void): this;
}

declare namespace - 名前空間の宣言

関連する型や関数をグループ化するために名前空間を使用します。jQueryのようなグローバルオブジェクトの型定義でよく使われます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
declare namespace MyLibrary {
  interface Config {
    debug: boolean;
    apiUrl: string;
  }

  function init(config: Config): void;
  function destroy(): void;

  const version: string;
}

// 使用例
MyLibrary.init({ debug: true, apiUrl: "https://api.example.com" });
console.log(MyLibrary.version);

declare module - モジュールの宣言

特定のモジュールパスに対する型定義を宣言します。型定義が存在しないパッケージに対して暫定的な型を付けたり、既存の型定義を拡張する際に使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 型定義のないモジュールに暫定的な型を付ける
declare module "untyped-library" {
  export function doSomething(value: string): void;
  export default class Client {
    connect(): Promise<void>;
  }
}

// ワイルドカードを使ったモジュール宣言(画像ファイルなど)
declare module "*.png" {
  const value: string;
  export default value;
}

declare module "*.svg" {
  const value: string;
  export default value;
}

自作ライブラリへの型定義追加

自作ライブラリに型定義を追加する方法は、プロジェクトの構成によって異なります。ここでは実践的なパターンを解説します。

TypeScriptで書かれたライブラリ

TypeScriptでライブラリを開発している場合、コンパイル時に.d.tsファイルを自動生成できます。

1
2
3
4
5
6
7
8
{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./dist/types",
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

package.jsonで型定義ファイルの場所を指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "name": "my-library",
  "main": "./dist/index.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    }
  }
}

JavaScriptライブラリに型定義を追加

JavaScriptで書かれた既存ライブラリに型定義を追加する場合、手動で.d.tsファイルを作成します。

以下は、シンプルなユーティリティライブラリの例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/utils.js
export function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export function slugify(str) {
  return str.toLowerCase().replace(/\s+/g, "-");
}

export const DEFAULT_LOCALE = "en-US";

対応する型定義ファイルを作成します。

1
2
3
4
// src/utils.d.ts
export declare function capitalize(str: string): string;
export declare function slugify(str: string): string;
export declare const DEFAULT_LOCALE: string;

プロジェクト内のカスタム型定義

プロジェクト固有の型定義は、専用のディレクトリにまとめて管理すると整理しやすくなります。

project/
├── src/
│   └── index.ts
├── types/
│   ├── global.d.ts
│   └── custom-modules.d.ts
└── tsconfig.json

tsconfig.jsonで型定義ディレクトリを参照に追加します。

1
2
3
4
5
6
{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  },
  "include": ["src/**/*", "types/**/*"]
}

グローバル型の拡張

TypeScriptでは、既存の型定義を拡張して独自のプロパティやメソッドを追加できます。これを「宣言マージ(Declaration Merging)」と呼びます。

Windowオブジェクトの拡張

ブラウザ環境でグローバル変数を追加する場合、Window インターフェースを拡張します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// types/global.d.ts
declare global {
  interface Window {
    __APP_CONFIG__: {
      apiUrl: string;
      environment: "development" | "staging" | "production";
    };
    gtag: (...args: unknown[]) => void;
  }
}

export {};

declare globalブロック内で定義することで、モジュールファイルからでもグローバル型を拡張できます。最後のexport {}は、このファイルをモジュールとして認識させるために必要です。

1
2
3
4
// src/config.ts
// グローバル型が拡張されているため、型安全にアクセスできる
const apiUrl = window.__APP_CONFIG__.apiUrl;
const env = window.__APP_CONFIG__.environment;

Node.jsのprocess.envの型付け

環境変数に型を付けることで、設定値へのアクセスを型安全にできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// types/env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: "development" | "production" | "test";
      DATABASE_URL: string;
      API_SECRET: string;
      PORT?: string;
    }
  }
}

export {};
1
2
3
4
// src/database.ts
// 環境変数が型付けされているため、補完が効く
const dbUrl = process.env.DATABASE_URL;
const port = process.env.PORT ?? "3000";

ライブラリの型定義を拡張

サードパーティライブラリの型定義に独自の拡張を加えることも可能です。以下はExpressのRequest型を拡張する例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// types/express.d.ts
import { User } from "../src/models/user";

declare global {
  namespace Express {
    interface Request {
      user?: User;
      sessionId: string;
    }
  }
}

export {};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";

export function authMiddleware(req: Request, res: Response, next: NextFunction) {
  // req.userが型安全に利用できる
  if (!req.user) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  next();
}

型定義が見つからない場合の対処法

サードパーティライブラリに型定義が存在しない場合の対処法を段階的に解説します。

対処法1: 暫定的なany型での宣言

最も簡易的な方法として、モジュール全体をany型として宣言できます。型安全性は失われますが、コンパイルエラーを回避できます。

1
2
// types/untyped-modules.d.ts
declare module "legacy-library";

この宣言により、ライブラリからのインポートは全てany型として扱われます。

対処法2: 最小限の型定義を作成

使用する機能のみに型を付ける方法です。完璧な型定義でなくても、よく使う部分だけでも型を付けておくと開発効率が向上します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// types/legacy-library.d.ts
declare module "legacy-library" {
  export interface Options {
    timeout?: number;
    retries?: number;
  }

  export function initialize(options?: Options): void;
  export function process(data: unknown): Promise<unknown>;

  const legacyLibrary: {
    initialize: typeof initialize;
    process: typeof process;
  };

  export default legacyLibrary;
}

対処法3: DefinitelyTypedへのコントリビュート

完全な型定義を作成した場合、DefinitelyTypedリポジトリにプルリクエストを送ることでコミュニティに貢献できます。

DefinitelyTypedへの貢献手順は以下の通りです。

  1. DefinitelyTypedリポジトリをフォーク
  2. types/パッケージ名ディレクトリを作成
  3. 必要なファイル(index.d.ts、tsconfig.json、テストファイル)を作成
  4. プルリクエストを作成

型定義ファイルの設計パターン

実践的な型定義ファイルを作成する際に役立つパターンを紹介します。

コールバック関数パターン

1
2
3
4
5
6
7
declare module "event-library" {
  type EventCallback<T = unknown> = (event: T) => void;
  type ErrorCallback = (error: Error) => void;

  export function on<T>(eventName: string, callback: EventCallback<T>): void;
  export function onError(callback: ErrorCallback): void;
}

ファクトリ関数パターン

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
declare module "factory-library" {
  interface ClientOptions {
    baseUrl: string;
    timeout?: number;
  }

  interface Client {
    get<T>(path: string): Promise<T>;
    post<T>(path: string, data: unknown): Promise<T>;
  }

  export function createClient(options: ClientOptions): Client;
}

プラグインパターン

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
declare module "plugin-system" {
  interface Plugin {
    name: string;
    initialize(): void;
    destroy(): void;
  }

  interface PluginHost {
    use(plugin: Plugin): this;
    getPlugin(name: string): Plugin | undefined;
  }

  export function createHost(): PluginHost;
}

型定義のトラブルシューティング

型定義に関するよくある問題と解決方法を解説します。

問題1: 型定義が認識されない

tsconfig.jsonの設定を確認してください。

1
2
3
4
5
6
7
8
{
  "compilerOptions": {
    "moduleResolution": "node",
    "typeRoots": ["./node_modules/@types", "./types"],
    "esModuleInterop": true
  },
  "include": ["src/**/*", "types/**/*"]
}

moduleResolutionが正しく設定されていないと、型定義が見つからない場合があります。

問題2: 重複する型定義

同じライブラリに対して複数の型定義が存在する場合、コンフリクトが発生することがあります。

1
2
3
4
5
{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

skipLibCheckを有効にすると、.d.tsファイルの型チェックをスキップし、コンフリクトを回避できます。ただし、これは根本的な解決ではないため、可能であれば重複する型定義を整理してください。

問題3: モジュールとスクリプトの混同

型定義ファイルがモジュールとして認識されない場合、export {}を追加します。

1
2
3
4
5
6
// これはスクリプトとして扱われる
declare const VERSION: string;

// モジュールにするには export を追加
declare const VERSION: string;
export {};

グローバル型を拡張する場合は、ファイルをモジュールとして認識させる必要があります。

まとめ

TypeScriptの型定義ファイル(.d.ts)について、以下の内容を解説しました。

  • 型定義ファイルの役割と基本構造
  • DefinitelyTypedと@typesパッケージの活用方法
  • declare文を使った各種宣言の書き方
  • 自作ライブラリへの型定義追加方法
  • グローバル型の拡張テクニック
  • 型定義が見つからない場合の対処法

型定義ファイルを理解することで、TypeScriptの型システムをより深く活用できるようになります。サードパーティライブラリを型安全に利用できるだけでなく、自作ライブラリを公開する際にも適切な型定義を提供できるようになります。

型定義の作成は最初は難しく感じるかもしれませんが、既存の@typesパッケージを参考にしながら少しずつ慣れていくことをお勧めします。

参考リンク