T-CREATOR

<div />

TypeScriptの型定義ファイルdtsを自作する使い方 破綻しない設計の基本

2025年12月29日
TypeScriptの型定義ファイルdtsを自作する使い方 破綻しない設計の基本

ライブラリを配布する際、型定義ファイル(.d.ts)の設計ミスで利用者の環境を壊してしまった経験はないでしょうか。この記事では、TypeScriptの型定義ファイルを自作する際の基本から、配布しても破綻しない設計のコツまでを実務経験をもとに解説します。declare、augmentation、モジュール宣言といった基本構文を整理しつつ、型安全を保ちながらメンテナンス可能な.d.tsファイルを作成する判断基準を提示します。tsconfig.jsonとの連携や、グローバル汚染を避ける使い方についても具体例を交えて説明するため、初めて型定義ファイルを作る方から、既存ライブラリの型定義を改善したい実務者まで、判断材料として活用できる内容です。

検証環境

  • OS: macOS 15.1 (Sequoia)
  • Node.js: 24.12.0 LTS
  • TypeScript: 5.9.3
  • 主要パッケージ:
    • @types/node: 22.10.5
  • 検証日: 2025 年 12 月 29 日

背景

なぜ型定義ファイルが必要になるのか

TypeScriptは型安全なコードを書くための仕組みを提供していますが、JavaScriptで書かれた既存ライブラリや、外部APIとのやり取りでは型情報が失われます。このギャップを埋めるのが型定義ファイル(.d.ts)です。

実務では以下のようなケースで型定義ファイルを自作する必要が生じます。

  • 社内共通ライブラリに型を付ける
  • 既存のJavaScriptコードをTypeScript化する過程で段階的に型を追加する
  • サードパーティライブラリの型定義が不完全、または存在しない
  • グローバル変数やプロセス環境変数に型を付けたい

型定義ファイルを正しく設計できれば、コード補完やコンパイル時のエラー検出が効くようになり、開発効率と品質が大きく向上します。一方で、設計を誤るとグローバルスコープの汚染や型の競合が発生し、利用者の環境を壊してしまうリスクがあります。

mermaidflowchart LR
  jsLib["JavaScriptライブラリ"] --> noType["型情報なし"]
  noType --> problem["補完なし<br/>実行時エラー"]
  dts["型定義ファイル<br/>.d.ts"] --> typed["型安全"]
  typed --> benefit["補完あり<br/>コンパイル時検出"]

型定義ファイルがあることで、JavaScriptライブラリでもTypeScriptの恩恵を受けられるようになります。

課題

型定義ファイル設計で実際に起きた問題

実務で型定義ファイルを作成・配布する際、以下のような問題に直面しました。

グローバル汚染による型競合

社内ライブラリに型定義を追加した際、グローバルスコープにdeclareで型を宣言したところ、利用者のプロジェクト内で同名の型が定義されていたため、型エラーが大量に発生しました。tsconfig.jsonのtypes設定を正しく行わず、意図しない型定義が読み込まれていたことが原因でした。

モジュール宣言の不備によるインポートエラー

declare moduleを使って外部ライブラリの型を拡張しようとした際、モジュール解決が正しく行われず、Cannot find moduleエラーが発生しました。moduleResolutionpathsの設定と、型定義ファイルの配置場所の関係を理解していなかったことが原因です。

augmentationの範囲制御ミス

既存の型に追加のプロパティを定義する際、declare globalnamespaceの使い分けを誤り、意図しない範囲まで型が拡張されてしまいました。特に、ライブラリ配布時には利用者の環境に影響を与えないよう、慎重な設計が必要でした。

これらの問題を放置すると、以下のリスクがあります。

  • 利用者のプロジェクトで予期しない型エラーが発生
  • ビルドが通らなくなる
  • 型推論が正しく働かず、型安全性が損なわれる
  • メンテナンスコストが増大し、型定義の更新が困難になる

解決策と判断

破綻しない型定義ファイル設計の基本方針

実務経験から、以下の設計方針を採用することで、配布しても壊れにくい型定義ファイルを作成できるようになりました。

方針1: モジュール宣言を優先し、グローバル宣言は最小限にする

グローバルスコープに型を追加すると、利用者の環境と競合するリスクが高まります。可能な限りdeclare moduleを使い、モジュールとしてエクスポートする形式を採用します。

どうしてもグローバルに宣言が必要な場合(例: windowオブジェクトの拡張、プロセス環境変数の型付け)は、declare globalで明示的に範囲を限定します。

方針2: augmentationは必要最小限の範囲で行う

既存の型を拡張する際、namespaceinterfaceのマージ機能を使いますが、拡張範囲を明確にすることが重要です。特に、サードパーティライブラリの型を拡張する場合は、利用者がオプトインできる設計にします。

方針3: tsconfig.jsonとの連携を意識する

型定義ファイルは、tsconfig.jsontypestypeRootsincludeexclude設定と密接に関係します。これらの設定を理解し、意図しない型定義が読み込まれないようにします。

実際に採用した設計例を、次章で具体的に説明します。

採用しなかった案とその理由

案1: すべての型をグローバルに宣言する

初期検討では、使いやすさを優先してすべての型をdeclareでグローバルに宣言することも検討しました。しかし、以下の理由で採用を見送りました。

  • 利用者のプロジェクトで型名が競合するリスクが高い
  • どこで型が定義されているか追跡しづらく、メンテナンス性が低い
  • TypeScriptのモジュールシステムの恩恵を受けられない

案2: 型定義をインラインで書く

型定義ファイルを作らず、実装ファイル内に型定義を含める方法も検討しました。これは小規模プロジェクトでは有効ですが、以下の理由で大規模には向きませんでした。

  • 型定義と実装が混在し、可読性が下がる
  • JavaScriptライブラリに後から型を追加する場合には使えない
  • 型定義のみを配布・共有することができない

具体例

この章では、実際に動作確認済みのコード例を通じて、型定義ファイルの使い方と設計パターンを解説します。

declare による基本的な型宣言

グローバル変数への型付け

ブラウザ環境でwindowオブジェクトに独自のプロパティを追加する場合、以下のように型を定義します。

typescript// global.d.ts
declare global {
  interface Window {
    myAppConfig: {
      apiEndpoint: string;
      version: string;
    };
  }
}

export {};

ポイント:

  • declare globalでグローバルスコープへの追加を明示
  • ファイル末尾のexport {}は、このファイルをモジュールとして扱うために必要
  • WindowインターフェースはTypeScript標準の型定義に存在するため、マージされる

つまずきポイント:

  • export {}を忘れると、ファイル全体がグローバルスクリプトとして扱われ、他のモジュール宣言と競合する可能性があります。

Node.js環境変数への型付け

プロセス環境変数に型を付ける場合、以下のように定義します。

typescript// env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      API_KEY: string;
      NODE_ENV: "development" | "production" | "test";
    }
  }
}

export {};

これにより、process.env.DATABASE_URLにアクセスする際に型補完が効き、存在しない環境変数にアクセスするとコンパイルエラーになります。

declare module によるモジュール型定義

外部ライブラリへの型付け

型定義が存在しないJavaScriptライブラリに型を付ける場合、以下のようにdeclare moduleを使います。

typescript// types/my-library.d.ts
declare module "my-library" {
  export interface Config {
    timeout: number;
    retries: number;
  }

  export function initialize(config: Config): void;
  export function getData(id: string): Promise<unknown>;
}

つまずきポイント:

  • モジュール名は、package.jsonnameフィールドまたは実際のインポートパスと完全に一致させる必要があります。

ワイルドカードモジュール宣言

CSSモジュールや画像ファイルをインポートする際の型定義は、ワイルドカードを使います。

typescript// types/assets.d.ts
declare module "*.module.css" {
  const classes: { [key: string]: string };
  export default classes;
}

declare module "*.png" {
  const src: string;
  export default src;
}

これにより、import styles from '.​/​Button.module.css'のようなインポートが型エラーなく使えます。

augmentation による型拡張

既存インターフェースの拡張

サードパーティライブラリの型を拡張する場合、以下のようにインターフェースのマージ機能を使います。

typescript// types/express-augmentation.d.ts
import { User } from "./models/User";

declare module "express-serve-static-core" {
  interface Request {
    currentUser?: User;
  }
}

実際に試したところ、この方法でExpressのRequestオブジェクトに独自プロパティを追加でき、ミドルウェアでreq.currentUserに型安全にアクセスできるようになりました。

mermaidflowchart TB
  original["元の型定義<br/>interface Request"]
  augment["拡張定義<br/>interface Request"]
  merged["マージ後<br/>currentUser追加"]

  original --> merged
  augment --> merged

TypeScriptのインターフェースマージ機能により、同名のインターフェースは自動的に統合されます。

つまずきポイント:

  • 拡張する型定義ファイルは、必ず元のモジュールをインポートしてからdeclare moduleを使う必要があります。インポートを忘れると、型のマージが正しく行われません。

tsconfig.json との連携設計

型定義ファイルが意図した通りに読み込まれるよう、tsconfig.jsonの設定を適切に行います。

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": false,
    "typeRoots": ["./node_modules/@types", "./types"],
    "types": ["node"]
  },
  "include": ["src/**/*", "types/**/*"],
  "exclude": ["node_modules", "dist"]
}

設定のポイント:

  • typeRoots: 型定義ファイルを探索するディレクトリを指定(デフォルトはnode_modules​/​@typesのみ)
  • types: 明示的に読み込む型定義パッケージを指定(空配列にするとすべての@typesを除外)
  • include: プロジェクトに含めるファイルパターン(型定義ディレクトリも含める)
  • skipLibCheck: falseにすることで、型定義ファイルの型チェックも行う

業務で問題になったのは、typeRootsを設定すると、デフォルトのnode_modules​/​@typesが除外されてしまう点です。両方を読み込むには、配列で明示的に両方を指定する必要があります。

配布用ライブラリの型定義設計

npm パッケージとして配布するライブラリの場合、以下の構成を採用しました。

cssmy-library/
├── dist/
│   ├── index.js
│   └── index.d.ts
├── src/
│   └── index.ts
├── package.json
└── tsconfig.json

package.jsonの設定:

json{
  "name": "my-library",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsc"
  }
}

tsconfig.json(ビルド用):

json{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "module": "ESNext",
    "target": "ES2022",
    "strict": true
  },
  "include": ["src"]
}

設計判断の理由:

  • declaration: trueで、TypeScriptコンパイル時に自動的に.d.tsファイルを生成
  • declarationMap: trueで、型定義から元のソースコードへのマッピングを生成(デバッグ時に便利)
  • typesフィールドで、型定義ファイルの位置を明示
  • files配列で、配布するファイルをdistディレクトリのみに限定

検証の結果、この構成であれば利用者がnpm install my-libraryした際に、自動的に型補完が効くことを確認しました。

設計パターンの選び方

型定義ファイルの設計には複数のアプローチがあります。ここでは、主要なパターンを比較し、選択の判断基準を示します。

グローバル宣言 vs モジュール宣言

パターン使用場面メリットデメリット実務での採用判断
グローバル宣言declareブラウザのwindowNode.jsのglobalprocess.envインポート不要どこからでもアクセス可型名競合リスクスコープ汚染必要最小限のみ使用
モジュール宣言declare modulenpm パッケージライブラリの型定義型名競合を回避明示的なインポートインポート文が必要基本的にこちらを優先

判断基準

以下のフローチャートで選択します。

mermaidflowchart TD
  start["型定義が必要"]
  check1{"グローバルオブジェクト<br/>への追加?"}
  check2{"既存ライブラリの<br/>拡張?"}

  globalDecl["declare global"]
  moduleDecl["declare module"]
  augment["augmentation<br/>with declare module"]

  start --> check1
  check1 -->|Yes| globalDecl
  check1 -->|No| check2
  check2 -->|Yes| augment
  check2 -->|No| moduleDecl

実際にプロジェクトで型定義を追加する際は、このフローに従って選択することで、一貫性のある設計を維持できています。

declare vs interface vs type

型を定義する際の構文選択についても、判断基準を整理します。

構文用途拡張性実務での使い分け
declare値の型宣言関数・変数の存在を示す-実装が別にある場合
interfaceオブジェクトの形状クラスの契約マージ可能拡張前提の型
type複雑な型の定義ユニオン・交差型マージ不可拡張不要の型

実務での選択例:

ライブラリ配布時、利用者が型を拡張できるようにする場合はinterfaceを使います。

typescript// ライブラリ側
export interface PluginConfig {
  name: string;
}

利用者側で拡張可能:

typescript// 利用者側
declare module "my-library" {
  interface PluginConfig {
    customOption?: boolean;
  }
}

一方、内部実装で使う型や、拡張を想定しない型はtypeを使います。

typescripttype InternalState = {
  readonly id: string;
  status: "idle" | "loading" | "success" | "error";
};

自動生成 vs 手書き

型定義ファイルの作成方法も、状況によって使い分けます。

方法向いているケースメリットデメリット
自動生成tsc --declarationTypeScriptで実装型情報が完全メンテナンス不要型と実装の一貫性設定が必要生成結果の調整が困難
手書きJavaScriptライブラリ外部APIグローバル拡張柔軟な型設計段階的な型付けメンテナンスコスト型と実装の不一致リスク

判断基準:

新規にTypeScriptでライブラリを作る場合は、必ず自動生成を採用します。検証の結果、手書きに比べて型と実装の不一致が発生しないため、長期的なメンテナンスコストが大幅に削減できました。

既存のJavaScriptコードに型を付ける場合は、手書きで段階的に型を追加していきます。ただし、将来的にTypeScriptへの移行を見据え、allowJscheckJsを有効にして、できる限りJSDocで型情報を付けておくと、移行時の負担が軽減されます。

まとめ

TypeScriptの型定義ファイル(.d.ts)を自作する際は、グローバルスコープの汚染を避け、モジュール宣言を優先することが破綻しない設計の基本です。declare、augmentation、declare moduleといった構文を正しく使い分け、tsconfig.jsonの設定と連携させることで、型安全を保ちながらメンテナンス可能な型定義を作成できます。

実務では、以下のポイントを意識することで、配布しても壊れにくい型定義ファイルを設計できました。

  • グローバル宣言は必要最小限にとどめ、できる限りモジュール宣言を使う
  • augmentationは範囲を明確にし、利用者がオプトインできる設計にする
  • tsconfig.jsonのtypeRoots、types、includeを適切に設定する
  • ライブラリ配布時は、自動生成(declaration: true)を優先する
  • 型定義と実装の一貫性を保つため、可能な限り型定義を自動生成する

ただし、プロジェクトの規模や既存コードベースの状況によって、最適な設計は異なります。段階的な型付けが必要な場合や、外部ライブラリの型拡張が必要な場合は、手書きの型定義ファイルが適しているケースもあります。この記事で示した判断基準を参考に、自分のプロジェクトに合った設計を選択してください。

型定義ファイルは一度作って終わりではなく、ライブラリの進化に合わせて継続的にメンテナンスが必要です。型と実装の不一致が発生しないよう、テストコードでの型チェックや、CI/CDでの型エラー検出を組み込むことをお勧めします。

関連リンク

Sources:

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;