【無料配布】10日間で学べるTypeScript学習資料

【TypeScript】declareとは?グローバル変数やモジュールの型定義

typescript-declare

TypeScript開発において、コンパイラが「そんな変数は存在しない!」とエラーが起きてるのに、ブラウザ上では正常に動いてるという現象に遭遇することがあります。

そのギャップを埋めるのが「declare」です。

本記事では、アンビエント宣言(Ambient Declarations)の基本から、グローバル変数の拡張、型定義のないライブラリの型付けまで解説します。

目次

TypeScriptのdeclareとは

TypeScriptでコードを書いていると、外部のスクリプトで定義された変数や関数を使いたい場面があります。

しかし、TypeScriptコンパイラは「自分が認識していないものは存在しない」と判断し、コンパイルエラーを吐き出します。

ここで登場するのがdeclare(宣言)というキーワードです。

TypeScriptのdeclare
  • コンパイラに対するアンビエント宣言
  • トランスパイル(JS出力)時には消滅する

コンパイラに対するアンビエント宣言

declareは、TypeScriptコンパイラに対して「この変数の実体は、TypeScriptの外側に存在しているから、エラーにしないで」と宣言する機能です。

実体の実装を伴わず「型情報だけ」を定義することをTypeScript用語で アンビエント宣言(Ambient Declarations)と呼びます。

トランスパイル(JS出力)時には消滅する

declareを使って記述されたコードは、「コンパイル時の型チェック」のためだけに存在します。

そのため、TypeScriptからJavaScriptへトランスパイル(変換)された際declareが付いた行は消滅します。

// TypeScriptのコード
declare const API_KEY: string;
console.log(API_KEY);

// トランスパイル後のJavaScriptコード
// declare const API_KEY: string; は出力されない
console.log(API_KEY);

実際にAPI_KEYが実行環境(ブラウザやNode.js)に存在してなければ、実行時にReferenceErrorが発生します。

declareは「存在を信じさせる」だけで、実体を作り出すわけではない点に注意が必要です。

なぜdeclareが必要なのか?

モダンな開発環境でもdeclareが必須となるシチュエーションは主に3つあります。

ユースケース
  • 型定義が存在しないJSライブラリの利用
  • CDN等で読み込まれたグローバル変数の認識
  • 画像ファイルやCSSなど非JSモジュールのインポート

型定義が存在しないJSライブラリの利用

npm経由でインストールしたJavaScriptライブラリの中には、@types/xxxのような型定義ファイルが提供されてないものがあります。

この場合、importすると「モジュールの型宣言が見つかりません」というエラーになります。

これを回避するためにdeclare moduleを使用します。

CDN等で読み込まれたグローバル変数の認識

Google Analytics(gtag)や外部のウィジェットなど、HTMLの<script>タグで直接読み込まれたJavaScriptは、グローバルオブジェクト(windowなど)に変数を生やします。

TypeScriptは検知できないため、declareを使ってグローバル変数の存在を教える必要があります。

画像ファイルやCSSなど非JSモジュールのインポート

WebpackやViteなどのバンドラーを使っていると、.png.cssを直接importできます。

しかし、TypeScriptはデフォルトでモジュールとして認識しないためdeclare module "*.png"のように宣言する必要があります。

declareの基本文法(変数・関数・クラス)

declareは様々な要素に対して付与できます。

共通するルールは「実装(中身の処理)を書いてはいけない」ということです。

declareの基本文法
  • declare const/let/var
  • declare function
  • declare classと名前空間(namespace)

declare const/let/var

外部で定義されたグローバル変数の型を定義します。

// OK: 型だけを宣言する
declare const MY_GLOBAL_CONFIG: { apiUrl: string; timeout: number };

// NG: 初期値(実装)を代入しようとするとエラーになる
// declare const MY_GLOBAL_CONFIG = { apiUrl: "http...", timeout: 1000 };

declare function

外部で定義された関数の型を定義します。

引数と戻り値の型だけを記述し、波括弧{}による処理の中身は書きません。

// OK: シグネチャ(型)のみ
declare function showModal(title: string, message: string): void;

// NG: 処理の中身を書くとエラー
// declare function showModal(title: string) { console.log(title); }

declare classと名前空間(namespace)

外部のクラスやグローバルオブジェクト(jQueryの$など)を定義する場合は classnamespaceを使用します。

declare class CustomGoogleMap {
  constructor(elementId: string);
  render(): void;
  setCenter(lat: number, lng: number): void;
}

// 呼び出し側は通常のクラスと同じように扱え、型補完も効く
const map = new CustomGoogleMap("map-container");
map.render();

グローバルオブジェクトの拡張とdeclare global

実務で直面しやすいのが、「windowオブジェクトに独自のプロパティを追加したいのにエラーが出てしまう」という問題です。

declare global
  • 既存のオブジェクトにプロパティを追加する
  • モジュールシステム下でのdeclare globalのルール

既存のオブジェクトにプロパティを追加する

ブラウザ環境において、window.dataLayer(GTM用)やwindow.webkitなどを触りたい場合、以下のように既存のWindowインターフェースを拡張します。

// interfaceの宣言マージ(Declaration Merging)を利用
interface Window {
  dataLayer: any[];
  myCustomApi: () => void;
}

// これでエラーが出なくなる
window.dataLayer = window.dataLayer || [];
window.myCustomApi();

Windowインターフェースは標準でグローバルに存在するため、同じ名前でinterface Windowを宣言すると既存の定義にプロパティが追加されます。

モジュールシステム下でのdeclare globalのルール

ファイル内にimportexportが一つでも存在すると、そのファイルは「モジュール」として扱われ、グローバルスコープから切り離されます。

モジュールファイル内からグローバルなWindowを拡張したい場合はdeclare globalブロックで囲む必要があります。

import { CustomData } from './types';

// ファイルがモジュールとして扱われているため、declare global が必要
declare global {
  interface Window {
    appData: CustomData;
  }
  
  // 新しいグローバル変数そのものを生やすことも可能
  const __INITIAL_STATE__: string;
}

window.appData = { id: 1 };
console.log(__INITIAL_STATE__); // エラーにならない

型定義がないnpmパッケージへの対応

マイナーなライブラリや社内製の古いライブラリをnpm installしてimportしようとすると型定義がないためエラーになります。

npmパッケージへの対応
  • サードパーティライブラリの型エラーを解消する
  • ワイルドカード(*)を用いたアセットファイルの型定義

サードパーティライブラリの型エラーを解消する

例えばawesome-legacy-libというパッケージに型定義がない場合、適当な.d.tsファイル(例:types.d.ts)を作成し、以下のように記述します。

// ひとまず any 型として扱わせ、コンパイルエラーを黙らせる(緊急回避)
declare module 'awesome-legacy-lib';

// --- または、真面目に型を付ける場合 ---
declare module 'awesome-legacy-lib' {
  export function doSomething(param: string): boolean;
  export const version: string;
}

declare module "モジュール名"と記述することでTypeScriptコンパイラに「この名前のモジュールは存在しこういう型を持っている」と認識できます。

ワイルドカード(*)を用いたアセットファイルの型定義

ReactやNext.js開発において、画像ファイルやCSS Modulesをインポートする際にもdeclare moduleが大活躍します。

// global.d.ts 等に記述
declare module '*.png' {
  const src: string;
  export default src;
}

declare module '*.module.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

ワイルドカード*を使うことで「.pngで終わる全てのファイルのインポートは、文字列をdefault exportするモジュールとして扱う」というルールを定義できます。

拡張子.d.ts(型定義ファイル)とdeclare

declareを使ったアンビエント宣言は、.tsファイルに書くこともできます。

一般的には.d.ts(Declaration File:型定義ファイル)にまとめて記述します。

拡張子.d.ts(型定義ファイル)
  • .tsファイルと.d.tsファイルの違い
  • .d.ts内での暗黙のdeclare

.tsファイルと.d.tsファイルの違い

  • .tsファイル
    ロジック(実装)と型定義の両方を含むファイル。
    コンパイルすると.jsファイルが出力される。
  • .d.tsファイル
    型情報だけを含むファイル。
    実装を書くことができず、コンパイルしても.jsファイルは出力されない。

DefinitelyTyped@types/reactなどのリポジトリ)で提供されるファイルは、全て.d.ts形式です。

.d.ts内での暗黙のdeclare

.d.tsファイルの特徴は、「トップレベルの宣言は暗黙的にdeclareが付与されたものとして扱われる」という点です。

// types/custom.d.ts

// 以下の変数は、先頭に declare を書かなくても自動的に declare const として扱われる
const GLOBAL_THEME: "light" | "dark";

// クラスも同様(実装は書けない)
class GlobalLogger {
  log(msg: string): void;
}

プロジェクト全体のグローバルな型定義や、外部モジュールの拡張を行う場合は、プロジェクトルート付近にtypes.d.tsglobal.d.tsといったファイルを作成します。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次