declare global とは、import や export のあるファイル(モジュール)の中から、グローバルの型に宣言を足すための書き方です。 window.appConfig のような独自プロパティや、globalThis に置いた変数、process.env のキーに型を付けたいときに使います。

この記事では、目的別にそのまま使える .d.ts を並べ、置く前と置いた後の tsc の出力、つまずきやすいエラー(TS2669・TS7017・TS2339 など)の全文と直し方をまとめます。ブラウザ側は Chromium で実際に値が読めることまで確かめました。

確認環境:Ubuntu 24.04、Node.js 22.22.2、TypeScript 7.0.2、@types/node 22.20.5、Playwright 1.56.0(Chromium 141)。

declare globalの早見表

やりたいこと declare global の中に書くもの 使う側
window にプロパティを足す interface Window { appConfig: {...} } window.appConfig
グローバル変数を足す var appVersion: string; appVersion / globalThis.appVersion / window.appVersion
process.env のキーに型を付ける namespace NodeJS { interface ProcessEnv { API_URL: string } } process.env.API_URL
既存の型にメソッドを足す interface Array<T> { last(): T | undefined } [1, 2].last()

エラーから直し方を引く表です。

TS番号 メッセージの要点 原因 直し方
TS2669 Augmentations for the global scope can only be directly nested in external modules ファイルに import も export も無い 先頭に export {}; を足す。または declare global を外して直接書く
TS2339 Property 'x' does not exist on type 'Window & typeof globalThis' 宣言が無い/.d.ts が読まれていない .d.ts を tsconfig の include に入れる
TS2339 Property 'x' does not exist on type 'typeof globalThis' let か const で宣言した var で宣言する
TS7017 type 'typeof globalThis' has no index signature globalThis.x の宣言がどこにも無い declare global に var x を足す
TS2304 Cannot find name 'x' グローバル変数の宣言が無い 同上
TS2717 Subsequent property declarations must have the same type 同じプロパティを違う型で2回宣言した 型をそろえるか、片方を消す

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScript declare globalの使い方|windowの型」

基本の形:export {} と declare global

まず、この記事で使う .d.ts の全体です。types/global.d.ts として置きます。

// types/global.d.ts
export {};

declare global {
  interface Window {
    appConfig: { apiBase: string; debug: boolean };
    dataLayer: unknown[];
  }

  var appVersion: string;

  interface Array<T> {
    last(): T | undefined;
  }
}

ポイントは3つです。

  • 1行目の export {}; は「このファイルはモジュールです」という印。declare global はモジュールの中でだけ書けます
  • interface は同じ名前で書くと合わさる(宣言のマージ)ので、もとの Window や Array の定義はそのままで、プロパティだけが増えます
  • グローバル変数は var で書きます(let と const で書いた場合の違いは後の節で)

tsconfig.json は次のとおりです。include に types フォルダを入れるのを忘れないようにします。

{
  "compilerOptions": {
    "target": "es2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "lib": ["es2022", "dom"],
    "types": [],
    "strict": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src", "types"]
}

windowにプロパティを足す

HTML 側の script で window.appConfig を用意し、TypeScript から読む例です。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>declare global</title>
  <script>
    window.appConfig = { apiBase: "/api/v1", debug: true };
    var appVersion = "1.4.0";
  </script>
</head>
<body>
  <pre id="out"></pre>
  <script type="module" src="./dist/app.js"></script>
</body>
</html>
// src/app.ts
Array.prototype.last = function <T>(this: T[]): T | undefined {
  return this[this.length - 1];
};

window.dataLayer = window.dataLayer ?? [];
window.dataLayer.push({ event: "page_view" });

const lines = [
  `apiBase: ${window.appConfig.apiBase}`,
  `debug: ${window.appConfig.debug}`,
  `appVersion: ${appVersion}`,
  `globalThis.appVersion: ${globalThis.appVersion}`,
  `window.appVersion: ${window.appVersion}`,
  `dataLayer.length: ${window.dataLayer.length}`,
  `[10, 20, 30].last(): ${[10, 20, 30].last()}`,
];
document.querySelector("#out")!.textContent = lines.join("\n");

.d.tsを置く前のエラー(全文)

types/global.d.ts が無い状態で npx tsc --noEmit を実行した結果です。

src/app.ts(1,17): error TS2339: Property 'last' does not exist on type 'any[]'.
src/app.ts(5,8): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'.
src/app.ts(5,27): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'.
src/app.ts(6,8): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'.
src/app.ts(9,22): error TS2339: Property 'appConfig' does not exist on type 'Window & typeof globalThis'.
src/app.ts(10,20): error TS2339: Property 'appConfig' does not exist on type 'Window & typeof globalThis'.
src/app.ts(11,18): error TS2304: Cannot find name 'appVersion'.
src/app.ts(12,40): error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
src/app.ts(13,32): error TS2339: Property 'appVersion' does not exist on type 'Window & typeof globalThis'.
src/app.ts(14,31): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'.
src/app.ts(15,40): error TS2339: Property 'last' does not exist on type 'number[]'.

npx tsc --noEmit --locale ja では、代表的な3種類が次のように表示されます。

src/app.ts(5,8): error TS2339: プロパティ 'dataLayer' は型 'Window & typeof globalThis' に存在しません。
src/app.ts(11,18): error TS2304: 名前 'appVersion' が見つかりません。
src/app.ts(12,40): error TS7017: 型 'typeof globalThis' にはインデックス シグネチャがないため、要素は暗黙的に 'any' 型になります。

同じ「宣言が無い」でも、アクセスのしかたで番号が変わります。

書き方 宣言が無いときのエラー
window.appVersion TS2339
appVersion(名前だけ) TS2304
globalThis.appVersion TS7017

.d.tsを置いた後:Chromiumでの実行結果

types/global.d.ts を置くと npx tsc はエラーなしで終わります。出力された dist/app.js を上の HTML から読み込み、Playwright で Chromium を起動して表示内容を取り出しました。

apiBase: /api/v1
debug: true
appVersion: 1.4.0
globalThis.appVersion: 1.4.0
window.appVersion: 1.4.0
dataLayer.length: 1
[10, 20, 30].last(): 30

確認に使ったスクリプトです(npm i -D playwright@1.56.0、ポートは 9350)。

// pw.mjs
import { chromium } from "playwright";
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
const root = process.argv[2];
const types = { ".html": "text/html; charset=utf-8", ".js": "text/javascript" };
const server = http.createServer((req, res) => {
  const file = path.join(root, req.url === "/" ? "index.html" : req.url);
  res.writeHead(200, { "content-type": types[path.extname(file)] });
  res.end(fs.readFileSync(file));
}).listen(9350);
const browser = await chromium.launch();
const page = await browser.newPage();
page.on("pageerror", (e) => console.log("pageerror:", e.message));
await page.goto("http://127.0.0.1:9350/");
console.log(await page.textContent("#out"));
console.log("evaluate:", await page.evaluate(() => JSON.stringify(window.appConfig)));
await browser.close();
server.close();

最後の evaluate の行は evaluate: {"apiBase":"/api/v1","debug":true} と表示されました。

declare global は型の宣言だけで、値は作りません。 window.appConfig に実際の値を入れているのは HTML 側の script です。値を入れる処理が無ければ、型チェックは通っても実行時には undefined になります。あとから入るかもしれない値は、次のように ? を付けておくと、使う側で確認を促してくれます。

// types/extra.d.ts
export {};
declare global {
  interface Window {
    readonly buildId: string;
    analytics?: { track(name: string): void };
  }
}
// src/x.ts
window.buildId = "b-2";
window.analytics.track("open");
window.analytics?.track("open");
export {};
src/x.ts(1,8): error TS2540: Cannot assign to 'buildId' because it is a read-only property.
src/x.ts(2,1): error TS18048: 'window.analytics' is possibly 'undefined'.

3行目の ?. を使った呼び出しはエラーになりません。

globalThisに変数を足す(varで書く理由)

declare global の中の var appVersion: string; は、次の3通りすべてのアクセスに型を付けます。

アクセス var で宣言 let / const で宣言
appVersion 通る 通る
globalThis.appVersion 通る TS2339
window.appVersion 通る TS2339

var を let に書き換えたときの出力です。

src/app.ts(12,40): error TS2339: Property 'appVersion' does not exist on type 'typeof globalThis'.
src/app.ts(13,32): error TS2339: Property 'appVersion' does not exist on type 'Window & typeof globalThis'.
src/app.ts(12,40): error TS2339: プロパティ 'appVersion' は型 'typeof globalThis' に存在しません。
src/app.ts(13,32): error TS2339: プロパティ 'appVersion' は型 'Window & typeof globalThis' に存在しません。

const でも同じ2行でした。これは JavaScript の動作と一致しています。スクリプトのトップレベルで var で作った変数はグローバルオブジェクトのプロパティになりますが、let と const で作った変数はなりません。Chromium で var a = 1; let b = 2; const c = 3; を script に書いて "a" in window などを調べると、var だけが true、let と const は false でした。型もそれに合わせてあるので、globalThis.〜 や window.〜 で読み書きするものは var で宣言します。

TypeScript 5.9.3 でも同じ2行(TS2339)になることを確かめました。一方、宣言そのものが無い名前に globalThis.〜 でアクセスした場合は TS7017 になります(次の節に全文があります)。

NodeのglobalThisとprocess.envを拡張する

Node.js 側の例です。@types/node を入れておきます(npm i -D @types/node)。

{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"],
    "strict": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src", "types"]
}
// types/global.d.ts
export {};

declare global {
  var appCache: Map<string, string>;
  var startedAt: number | undefined;

  namespace NodeJS {
    interface ProcessEnv {
      readonly NODE_ENV: "development" | "production" | "test";
      readonly API_URL: string;
      readonly PORT?: string;
    }
  }
}
// src/main.ts
globalThis.appCache ??= new Map();
globalThis.startedAt ??= Date.now();
appCache.set("greeting", "こんにちは");

const url: string = process.env.API_URL;
const port: number = Number(process.env.PORT ?? "3000");
const mode = process.env.NODE_ENV;

console.log(appCache.get("greeting"), url, port, mode);
console.log(typeof globalThis.startedAt, Object.hasOwn(globalThis, "appCache"));
$ npx tsc
$ API_URL=http://localhost:9351 NODE_ENV=development node dist/main.js
こんにちは http://localhost:9351 3000 development
number true

.d.tsを置く前のエラー(全文)

src/main.ts(1,12): error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
src/main.ts(2,12): error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
src/main.ts(3,1): error TS2304: Cannot find name 'appCache'.
src/main.ts(5,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.
src/main.ts(9,13): error TS2304: Cannot find name 'appCache'.
src/main.ts(10,31): error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.

process.env.API_URL は、拡張する前は string | undefined です。ProcessEnv に API_URL: string を足すと string として扱えるようになり、5行目の TS2322 が消えます。

拡張するとキーの打ち間違いにも気づける

// src/t.ts
const a: string = process.env.API_ULR;
const b: string = process.env.PORT;
if (process.env.NODE_ENV === "staging") {}
globalThis.appCahce = new Map();
export {};
src/t.ts(1,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.
src/t.ts(2,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.
src/t.ts(3,5): error TS2367: This comparison appears to be unintentional because the types '"development" | "production" | "test"' and '"staging"' have no overlap.
src/t.ts(4,12): error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
行 何が起きたか
1 API_ULR(打ち間違い)は宣言に無いので string | undefined のまま
2 PORT? は省略可能と宣言したので undefined の可能性が残る
3 NODE_ENV に無い値 "staging" との比較を指摘
4 appCahce(打ち間違い)は宣言に無い

型は約束で、値の確認は別に行う

環境変数を渡さずに同じ main.js を動かすと、こうなります。

$ node dist/main.js
こんにちは undefined 3000 undefined
number true

型では API_URL: string ですが、実行時に環境変数が無ければ undefined です。ProcessEnv の拡張は「このキーは設定されている」という約束を型に書くだけなので、起動時に必須の環境変数がそろっているかを確かめる処理を別に用意しておくと安心です。

既存の型(Arrayなど)にメソッドを足す

最初の global.d.ts に入れた interface Array<T> { last(): T | undefined; } がこれにあたります。公式ハンドブックの「Global augmentation」にも、Array にメソッドを足す例が載っています。

// 型の宣言(types/global.d.ts の declare global の中)
interface Array<T> {
  last(): T | undefined;
}
// 値の実装(src/app.ts)
Array.prototype.last = function <T>(this: T[]): T | undefined {
  return this[this.length - 1];
};
書くもの 役割 無いとどうなるか
interface への追加 型を足す TS2339(Property 'last' does not exist on type 'number[]')
prototype への代入 実行時の中身を足す 型チェックは通るが、実行時に関数が無い

型と実装の両方がそろって初めて動きます。 標準の型にメソッドを足すと、あとから同じ名前のメソッドが言語に入ったときに重なる可能性があるため、last(array) のような普通の関数で足りる場面ではそちらが手軽です。

export {} が無いとき:TS2669

global.d.ts の先頭から export {}; を消すと、次のエラーになります(先頭の11行は「.d.tsを置く前」と同じ内容です)。

…(省略:TS2339 などが11行)…
types/global.d.ts(1,9): error TS2669: Augmentations for the global scope can only be directly nested in external modules or ambient module declarations.
types/global.d.ts(1,9): error TS2669: グローバル スコープの拡張を直接入れ子にできるのは、外部モジュールまたは環境モジュールの宣言内のみです。

TypeScript は、トップレベルに import か export が1つでもあるファイルをモジュール、無いファイルをスクリプトとして扱います。declare global は「モジュールの中からグローバルに出ていく」ための書き方なので、スクリプトの中には書けません。直し方は2通りあります。

直し方 書き方 向いている場面
モジュールにする 先頭に export {}; を足す そのファイルで import を使いたい/今後使う可能性がある
declare global を外す 中身をトップレベルに直接書く import を使わない小さな宣言ファイル

2つめの書き方(スクリプト形式)はこうなります。こちらもエラーなしで通りました。

// types/global.d.ts(スクリプト形式)
interface Window {
  appConfig: { apiBase: string; debug: boolean };
  dataLayer: unknown[];
}

declare var appVersion: string;

interface Array<T> {
  last(): T | undefined;
}

スクリプト形式のファイルは、あとから import type { Foo } from "./foo.js"; のような1行を足した時点でモジュールに変わり、中の宣言がグローバルに届かなくなります。迷ったら export {}; + declare global の形にそろえておくと、import を足しても動作が変わりません。

.tsファイルの中に書いてもよい

declare global は .d.ts 専用ではなく、import や export のある普通の .ts にも書けます。

// src/y.ts
import { helper } from "./z.js";

declare global {
  interface Window {
    helperReady: boolean;
  }
}

window.helperReady = helper();

値を入れるコードのすぐそばに型を書けるので、「このプロパティはどこで設定されるのか」が追いやすくなります。

.d.tsがincludeから外れたとき:TS2339

.d.ts の中身が正しくても、tsc がそのファイルを読んでいなければ宣言は無いのと同じです。tsconfig.json の include を ["src"] だけにすると、置く前と同じ11行のエラーに戻りました。

src/app.ts(1,17): error TS2339: Property 'last' does not exist on type 'any[]'.
src/app.ts(5,8): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'.
…(省略:全11行)…

読まれているかどうかは --listFilesOnly で確かめられます(node_modules 内の行は除き、パスは一般的な名前に置き換えています)。

$ npx tsc --noEmit --listFilesOnly
/home/taro/app/src/app.ts

include を ["src", "types"] に戻すと、global.d.ts が一覧に現れます。

$ npx tsc --noEmit --listFilesOnly
/home/taro/app/src/app.ts
/home/taro/app/types/global.d.ts

「.d.ts を書いたのに TS2339 が消えない」ときは、まずこの一覧にファイルがあるかを見るのが近道です。

同じ名前を違う型で宣言したとき:TS2717

interface のマージはプロパティを「足す」仕組みで、すでにあるプロパティの型を変えることはできません。別のファイルで同じプロパティを違う型で宣言すると、次のようになります。

// types/extra.d.ts
export {};

declare global {
  interface Window {
    appConfig: { apiBase: string };
    title: string;
  }
  var appVersion: number;
}
src/app.ts(10,30): error TS2339: Property 'debug' does not exist on type '{ apiBase: string; }'.
types/global.d.ts(5,5): error TS2717: Subsequent property declarations must have the same type.  Property 'appConfig' must be of type '{ apiBase: string; }', but here has type '{ apiBase: string; debug: boolean; }'.
types/global.d.ts(9,7): error TS2403: Subsequent variable declarations must have the same type.  Variable 'appVersion' must be of type 'number', but here has type 'string'.
types/global.d.ts(5,5): error TS2717: 後続のプロパティ宣言は同じ型でなければなりません。プロパティ 'appConfig' の型は '{ apiBase: string; }' である必要がありますが、ここでは型が '{ apiBase: string; debug: boolean; }' になっています。
types/global.d.ts(9,7): error TS2403: 後続の変数宣言は同じ型でなければなりません。変数 'appVersion' の型は 'number' である必要がありますが、'string' になっています。

プロパティは TS2717、var は TS2403 です。ライブラリの型定義がすでに同じ名前を宣言している場合にも出るので、そのときは別の名前にするか、ライブラリ側の型に合わせます。

書き方の選び方

状況 選ぶ書き方
HTML の script やタグ管理ツールが window に値を入れる interface Window に足す
自分のコードが globalThis に値を置く(キャッシュなど) var で宣言する
環境変数のキーを補完したい NodeJS.ProcessEnv に足す
ファイルをまたいで値を共有したいだけ グローバルを使わず、モジュールから export する

グローバルに置いた値はどこからでも読み書きできるぶん、どこで変わったかを追うのに手間がかかります。外部のスクリプトとの受け渡しなど、グローバルが必要な場面に絞って declare global を使い、それ以外は export / import で渡すと、型も自然に付きます。

よくある質問

Q. declare global を書くファイルは .d.ts と .ts のどちらがよいですか? A. どちらでも動きます。複数のファイルから使う宣言は types/global.d.ts のように1か所へ、特定のファイルが値を入れるプロパティはその .ts の中に書くと追いやすくなります。

Q. export {}; は何をしているのですか? A. 何も export しない文ですが、これがあるとファイルがモジュールとして扱われます。declare global はモジュールの中でだけ書けるため、import も export も無いファイルではこの1行が必要です(無いと TS2669)。

Q. (window as any).foo と書くのと何が違いますか? A. as any は型チェックをその場で外す書き方で、プロパティ名の打ち間違いも通ります。declare global で宣言しておくと、補完が効き、打ち間違いは TS2339 で知らせてくれます。

Q. .d.ts を書いたのに TS2339 が消えません。 A. npx tsc --noEmit --listFilesOnly の一覧にその .d.ts があるかを確かめます。無ければ tsconfig.json の include にフォルダを足します。一覧にあるのに消えない場合は、export {}; があるのに declare global で囲んでいない、という組み合わせになっていないかを見ます。

まとめ

  • declare global は、モジュールの中からグローバルの型に宣言を足す書き方。型だけを足し、値は作らない
  • window のプロパティは interface Window、グローバル変数は var、環境変数は NodeJS.ProcessEnv に足す
  • let / const で宣言すると globalThis.〜 と window.〜 が TS2339 になる
  • import も export も無いファイルに書くと TS2669。export {}; を足すか、declare global を外して直接書く
  • .d.ts が include から外れていると TS2339 のまま。--listFilesOnly で読まれているかを確かめる
  • 同じプロパティを違う型で宣言すると TS2717(var は TS2403)
  • 型は約束なので、実行時に値が入っているかの確認は別に用意する

参考・出典

  • TypeScript 公式ハンドブック「Declaration Merging」(Module Augmentation / Global augmentation) https://www.typescriptlang.org/docs/handbook/declaration-merging.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScript declare globalの使い方|windowの型」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?