- TypeScript
TypeScript 型定義ファイル(d.ts)の書き方とTS7016の直し方
型定義ファイル(d.ts)とは、JavaScriptで書かれたライブラリや、実行時にだけ存在する値について「どんな型か」だけを TypeScript に伝えるためのファイルです。 中身は型の宣言だけで、JavaScript には変換されません。import したライブラリに型定義が見つからないと error TS7016: Could not find a declaration file for module ... が表示されますが、これは @types を入れる → declare module を1行書く → 型を自分で書く、の3段階のどれかで解消できます。
この記事では、TS7016 のエラー全文を手元で再現したうえで、3段階の直し方をコピペできる形で示します。あわせて「d.ts を書いたのに効かない」ときの原因を1つずつ再現し、チェック表にまとめました。掲載しているエラー文と出力は、すべて TypeScript 5.9.3(tsc)で実際に実行して取得したものです。 主要な3点は npm の latest である TypeScript 7.0.2 でも同じ結果になることを確かめています。
型定義ファイル(d.ts)とTS7016の早見表
まず「いま起きていること」から対処を引けるようにまとめます。
| 症状・やりたいこと | 対処 | 書くもの(最小形) |
|---|---|---|
| TS7016 が出た(npm のライブラリ) | 段階1:@types を入れる | npm i -D @types/lodash |
| @types が存在しない | 段階2:1行で通す(中身は any) | declare module "legacy-lib"; |
| 補完と型チェックも効かせたい | 段階3:型を自分で書く | declare module "legacy-lib" { export function ... } |
| d.ts を置いたのに TS7016 が消えない | include に d.ts のフォルダを足す | "include": ["src", "types"] |
| d.ts に import を足したら効かなくなった | declare global で囲む、または import("...") 型を使う |
export {}; declare global { ... } |
./logo.svg の import で TS2307 |
ワイルドカード宣言 | declare module "*.svg" { ... } |
window.xxx で TS2339 |
Window を拡張 | interface Window { xxx: ... } |
process.env.XXX に型を付けたい |
ProcessEnv を拡張 | declare namespace NodeJS { interface ProcessEnv { ... } } |
Express の req.userId で TS2339 |
Request を拡張 | declare global { namespace Express { interface Request { ... } } } |
| 自作の .js に型を付けたい | 同じ名前の d.ts を隣に置く | calc.js の隣に calc.d.ts |
| d.ts が読まれているか知りたい | 読み込んだファイルを一覧 | npx tsc -p . --listFiles |
エラー番号ごとの意味も並べておきます(どれもこの記事の中で再現しています)。
| エラー番号 | 出る場面 | 主な直し方 |
|---|---|---|
| TS7016 | import 先の JavaScript は見つかったが、型定義が見つからない | @types を入れる/declare module を書く/include を見直す |
| TS2307 | import 先そのものが見つからない(svg・css など) | ワイルドカード宣言、moduleResolution の見直し |
| TS2339 | Window や Request に無いプロパティを使った | interface を拡張する |
| TS2304 | グローバル変数の宣言が見えていない | d.ts がグローバル扱いかを確認する |
| TS2591 / TS2580 | process などの型が見えていない |
types・typeRoots の指定を見直す |
| TS2665 / TS2669 | d.ts の中の書き方がモジュール扱いと合っていない | export {} と declare global の組み合わせを直す |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript 型定義ファイル(d.ts)の書き方とTS7016の直し方」
確認に使った環境
| 項目 | 版 |
|---|---|
| Node.js | 22.22.2 |
| TypeScript | 5.9.3(一部を 7.0.2 でも確認) |
| lodash / @types/lodash | 4.18.1 / 4.17.25 |
| express / @types/express | 5.2.1 / 5.0.6 |
| @types/node | 22.20.5 |
基準にした tsconfig.json は次のとおりです。以降の例は、断りがない限りこの設定で npx tsc -p . を実行しています。
{
"compilerOptions": {
"target": "ES2022",
"module": "node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}
「型定義が無いライブラリ」の例として、node_modules/legacy-lib に JavaScript だけの小さなパッケージを手で置きました(型定義なしのライブラリを再現するための架空のパッケージです)。
// node_modules/legacy-lib/index.js
exports.slugify = (s, opt) => String(s).trim().toLowerCase().replace(/\s+/g, (opt && opt.separator) || '-');
exports.version = '1.0.0';
TS7016を再現する(エラー全文)
lodash(@types を入れていない状態)と legacy-lib を import します。
// src/a.ts
import _ from "lodash";
import { slugify } from "legacy-lib";
console.log(_.chunk([1, 2, 3, 4], 2));
console.log(slugify("Hello World"));
$ npx tsc -p .
src/a.ts(1,15): error TS7016: Could not find a declaration file for module 'lodash'. 'node_modules/lodash/lodash.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/lodash` if it exists or add a new declaration (.d.ts) file containing `declare module 'lodash';`
src/a.ts(2,25): error TS7016: Could not find a declaration file for module 'legacy-lib'. 'node_modules/legacy-lib/index.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/legacy-lib` if it exists or add a new declaration (.d.ts) file containing `declare module 'legacy-lib';`
エラー文は3つの情報でできています。
| エラー文の部分 | 意味 |
|---|---|
| Could not find a declaration file for module 'lodash' | lodash の型定義(d.ts)が見つからない |
| 'node_modules/lodash/lodash.js' implicitly has an 'any' type | JavaScript 本体は見つかった。このままだと any として扱うことになる |
Try npm i --save-dev @types/lodash ... or add ... declare module 'lodash'; |
直し方の候補が2つ書いてある |
TS7016 は「ライブラリが無い」のではなく「型だけが無い」というお知らせです。 実行時の動作には関係しないので、型を用意すれば解消します。なお、このエラーは「暗黙の any」を知らせるものなので、noImplicitAny が有効(strict: true に含まれます)のときに出ます。同じコードを --noImplicitAny false で実行するとエラー0件で通りましたが、import したものがすべて any になり型チェックが働かなくなるため、次の3段階で型を用意するほうが安心です。
TS7016を3段階で直す
flowchart TD
A[TS7016 が出た] --> B{"@types/パッケージ名 が<br>npm にある?"}
B -- ある --> C[段階1: npm i -D で入れる]
B -- ない --> D{補完や型チェックが<br>必要?}
D -- まず通したい --> E[段階2: declare module を1行]
D -- 必要 --> F[段階3: 使う関数の型を書く]
E -. あとから育てる .-> F
| 段階 | 方法 | 手間 | 型の安全性 | 向いている場面 |
|---|---|---|---|---|
| 1 | @types を入れる | 小 | 高い | @types が公開されているライブラリ |
| 2 | declare module "名前"; を1行 |
小 | any になる | すぐにビルドを通したいとき、試しに使うとき |
| 3 | 使う部分の型を書く | 中 | 自分で書いた範囲は高い | 長く使うライブラリ、社内の JavaScript |
段階1:@types を入れる
エラー文の案内どおり、@types/パッケージ名 を開発用の依存として入れます。
npm i --save-dev @types/lodash
$ npx tsc -p .
src/a.ts(2,25): error TS7016: Could not find a declaration file for module 'legacy-lib'. 'node_modules/legacy-lib/index.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/legacy-lib` if it exists or add a new declaration (.d.ts) file containing `declare module 'legacy-lib';`
lodash のエラーが消え、legacy-lib の1件だけになりました。@types のパッケージは node_modules/@types に入り、tsconfig を変えなくても自動で読み込まれます。 ライブラリ自体が型定義を同梱している場合(package.json に types がある場合)は、@types を入れなくても最初からエラーになりません。
段階2:declare module を1行書く
@types が無いライブラリは、d.ts を1つ作って次の1行を書きます。置き場所は include に含まれるフォルダ(ここでは src)です。
// src/legacy-lib.d.ts
declare module "legacy-lib";
$ npx tsc -p .
(エラー0件)
この書き方は「このモジュールは存在する。中身はすべて any」という宣言です。手早い反面、何を書いても通る点は知っておきましょう。次のコードは、存在しない nothing を import し、引数も戻り値の型も合っていませんが、エラー0件でした。
// src/b.ts
import { slugify, nothing } from "legacy-lib";
const n: number = slugify(123, 456);
nothing.anything();
段階3:使う関数の型を自分で書く
同じ d.ts を、波かっこ付きの形に書き換えます。全部を書く必要はなく、自分が使う関数だけで十分です。
// src/legacy-lib.d.ts
declare module "legacy-lib" {
export interface SlugifyOptions {
separator?: string;
}
export function slugify(text: string, options?: SlugifyOptions): string;
export const version: string;
}
src/a.ts はエラー0件で通り、JavaScript に変換して実行した結果は次のとおりでした。
[ [ 1, 2 ], [ 3, 4 ] ]
hello-world
今度は、型に合わない使い方がきちんと検出されます。
// src/b.ts
import { slugify, nothing } from "legacy-lib";
const n: number = slugify("Hello World", { separator: "_" });
slugify(123);
src/b.ts(1,19): error TS2305: Module '"legacy-lib"' has no exported member 'nothing'.
src/b.ts(3,7): error TS2322: Type 'string' is not assignable to type 'number'.
src/b.ts(4,9): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
段階2では通っていた3か所が、段階3ではすべてエラーとして見つかりました。まず段階2で通し、よく使う関数から段階3に育てていく進め方が取り組みやすいです。
d.tsの書き方の基本(declareの一覧)
d.ts の中では、declare を付けて「実体は別の場所にある」ことを伝えます。よく使う形を一覧にします。
| 宣言したいもの | 書き方 | 使う側 |
|---|---|---|
| グローバル変数 | declare const APP_VERSION: string; |
APP_VERSION |
| グローバル関数 | declare function track(name: string): void; |
track("view") |
| グローバルな型 | interface User { id: string } |
const u: User = ... |
| 既存のグローバル型に追加 | interface Window { appConfig: ... } |
window.appConfig |
| npm パッケージ(中身は any) | declare module "legacy-lib"; |
import x from "legacy-lib" |
| npm パッケージ(型つき) | declare module "legacy-lib" { export ... } |
import { slugify } from "legacy-lib" |
| 拡張子ごとの import | declare module "*.svg" { ... } |
import logo from "./logo.svg" |
| 名前空間 | declare namespace NodeJS { ... } |
process.env.API_URL |
グローバル扱いとモジュール扱いの違い
d.ts を書くうえで最も大切なのが、次の決まりです。
ファイルの一番外側に import か export が1つでもあると、そのファイルは「モジュール」として扱われ、中の宣言はファイルの外から見えなくなります。 import も export も無いファイルは「スクリプト」として扱われ、宣言はそのままグローバルになります。
| d.ts の状態 | 扱い | 一番外側の interface Window {...} |
declare module "x" {...} |
|---|---|---|---|
| import / export が無い | スクリプト(グローバル) | 既存の Window に追加される | 新しいモジュールの宣言になる |
| import / export がある | モジュール | そのファイル内だけの別の Window になる | 既存モジュールへの「追加」として扱われる |
この違いが「d.ts を書いたのに効かない」原因の多くを占めます。次の章で実際に再現します。
d.tsを書いたのに効かない原因チェック表
上から順に確かめると、切り分けが早く進みます。
| No. | 確かめること | よく出るエラー | 直し方 |
|---|---|---|---|
| 1 | d.ts が include の対象フォルダにあるか | TS7016 のまま | include に d.ts のフォルダを足す |
| 2 | tsc src/a.ts のようにファイル名を渡していないか |
TS7016 のまま | tsc -p . で実行する(ファイル名を渡すと tsconfig.json は読まれない) |
| 3 | declare module を書いた d.ts の外側に import / export がないか |
TS7016 のまま(内部では TS2665) | 外側の import をやめ、import("...") 型を使う |
| 4 | グローバル宣言の d.ts に import / export がないか | TS2304、TS2339 | declare global { } で囲む |
| 5 | declare global だけ書いて export が無い状態になっていないか |
TS2304、TS2339(内部では TS2669) | 先頭に export {}; を足す |
| 6 | types を空や一部だけにしていないか |
TS2591 | 必要な名前を types に足す |
| 7 | typeRoots に自作フォルダだけを書いていないか |
TS2580 | ./node_modules/@types も並べる |
| 8 | typeRoots のフォルダに d.ts を直接置いていないか | TS7016 のまま | フォルダ/パッケージ名/index.d.ts の形にするか、include に足す |
| 9 | moduleResolution が node10 のまま、exports のサブパスを import していないか | TS2307 | node16 / nodenext / bundler にする |
| 10 | ライブラリの exports に types 条件があるか | TS7016(There are types at ... と続く) | ライブラリ側の更新、または declare module で補う |
| 11 | skipLibCheck が true で、d.ts 自体のエラーが隠れていないか | 画面には出ない | 一時的に --skipLibCheck false で実行する |
以下、それぞれを再現します。
原因1:include にd.tsのフォルダが入っていない
段階3で作った d.ts を、src/ から types/ に移してみます。tsconfig.json の include は ["src"] のままです。
$ npx tsc -p .
src/a.ts(2,25): error TS7016: Could not find a declaration file for module 'legacy-lib'. 'node_modules/legacy-lib/index.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/legacy-lib` if it exists or add a new declaration (.d.ts) file containing `declare module 'legacy-lib';`
--listFiles で読み込まれたファイルを見ると(node_modules 配下を除いて表示)、src/a.ts しか出てきません。d.ts が読まれていないことがわかります。
$ npx tsc -p . --listFiles | grep -v node_modules
src/a.ts
include に types を足します。
{
"include": ["src", "types"]
}
$ npx tsc -p .
(エラー0件)
$ npx tsc -p . --listFiles | grep -v node_modules
src/a.ts
types/legacy-lib.d.ts
--explainFiles を使うと、読み込まれた理由まで表示されます。
types/legacy-lib.d.ts
Matched by include pattern 'types' in 'tsconfig.json'
declare module を書いた d.ts は、どこからも import されないので、include(または files)に入っていないと読まれません。 「置いたのに効かない」ときは、まず --listFiles に自分の d.ts が出てくるかを確かめるのが近道です(出力のパスは、見やすいように作業フォルダからの相対パスに置き換えています)。
原因2:tscにファイル名を渡している
include を直したあとでも、次のようにファイル名を付けて実行すると TS7016 が再び出ました(以降、TS7016 の2行目にある案内文は省略して載せます)。
$ npx tsc --noEmit --strict --esModuleInterop --module node16 src/a.ts
src/a.ts(2,25): error TS7016: Could not find a declaration file for module 'legacy-lib'. 'node_modules/legacy-lib/index.js' implicitly has an 'any' type.
コマンドラインでファイル名を渡すと tsconfig.json は使われず、include も効きません。 設定を使うときは npx tsc -p .(または引数なしの npx tsc)で実行します。
原因3:declare module のd.tsに import を足した
型を別ファイルに分けようとして、d.ts の先頭に import を書いた例です。
// types/options.d.ts
export interface SlugifyOptions {
separator?: string;
}
// types/legacy-lib.d.ts(うまくいかない例)
import type { SlugifyOptions } from "./options";
declare module "legacy-lib" {
export function slugify(text: string, options?: SlugifyOptions): string;
export const version: string;
}
$ npx tsc -p .
src/a.ts(2,25): error TS7016: Could not find a declaration file for module 'legacy-lib'. 'node_modules/legacy-lib/index.js' implicitly has an 'any' type.
d.ts は include に入っているのに、TS7016 に戻ってしまいました。理由は --skipLibCheck false を付けるとわかります。
$ npx tsc -p . --skipLibCheck false
(TS7016 に加えて次の1行が表示される)
types/legacy-lib.d.ts(3,16): error TS2665: Invalid module name in augmentation. Module 'legacy-lib' resolves to an untyped module at 'node_modules/legacy-lib/index.js', which cannot be augmented.
外側に import があるため、このファイルはモジュール扱いになり、declare module "legacy-lib" が「新しい宣言」ではなく「既存の型への追加(augmentation)」と解釈されました。legacy-lib には元になる型が無いので追加できない、というのが TS2665 の内容です。skipLibCheck が true だと d.ts 自体のエラーは表示されないため、原因が見えにくくなります。
直し方は、外側の import をやめて import("...") 型を使う形です。
// types/legacy-lib.d.ts(直した形)
declare module "legacy-lib" {
export function slugify(text: string, options?: import("./options").SlugifyOptions): string;
export const version: string;
}
$ npx tsc -p . --skipLibCheck false
(エラー0件)
なお、import 文を declare module の波かっこの中へ移す方法は、相対パスでは使えませんでした(TS2439: Import or export declaration in an ambient module declaration cannot reference module through relative module name.)。相対パスの型を使うときは import("...") 型か、同じ d.ts の中に型を書く形にします。
原因4・5:グローバル宣言のd.tsに export がある/無い
グローバル変数と Window の拡張を使うコードです(--lib es2022,dom を付けて実行)。
// src/g.ts
console.log(APP_VERSION);
console.log(window.appConfig.apiBase);
4通りの d.ts で結果を比べました。
| d.ts の書き方 | 結果 |
|---|---|
| A:import / export なしで、そのまま宣言 | エラー0件 |
B:A に export type Id = string; を1行足した |
TS2304、TS2339 |
C:export があり、宣言を declare global { } で囲んだ |
エラー0件 |
D:declare global { } だけで、export が無い |
TS2304、TS2339(skipLibCheck false で TS2669 も表示) |
// A:スクリプト扱い(そのままグローバルになる)
interface Window {
appConfig: { apiBase: string };
}
declare const APP_VERSION: string;
// B:export を1行足しただけで、宣言が外から見えなくなる
export type Id = string;
interface Window {
appConfig: { apiBase: string };
}
declare const APP_VERSION: string;
src/g.ts(1,13): error TS2304: Cannot find name 'APP_VERSION'.
src/g.ts(2,20): error TS2339: Property 'appConfig' does not exist on type 'Window & typeof globalThis'.
// C:モジュール扱いのファイルからグローバルに宣言する
export type Id = string;
declare global {
interface Window {
appConfig: { apiBase: string };
}
const APP_VERSION: string;
}
D の形(declare global だけ)では、--skipLibCheck false で次のエラーが表示されました。
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.
declare global はモジュール扱いのファイルの中でだけ使えます。 export するものが無いときは、先頭に export {}; を1行置いてモジュール扱いにします。まとめると次の2つの型のどちらかに揃えるのが確実です。
| 型 | 書き方 | 使いどころ |
|---|---|---|
| スクリプト型 | import / export を書かず、そのまま宣言 | 他の型を import しなくてよいとき |
| モジュール型 | export {};(または import)+ declare global { } |
他のファイルの型を import して使いたいとき |
原因6・7・8:types と typeRoots の指定
process.env を使う1行で確かめます(@types/node は導入済み)。
// src/n.ts
console.log(process.env.API_URL);
| tsconfig の指定 | 結果 |
|---|---|
| types・typeRoots とも指定なし | エラー0件 |
"types": [] |
TS2591 |
"types": ["node"] |
エラー0件 |
"typeRoots": ["./types"] |
TS2580 |
"typeRoots": ["./types", "./node_modules/@types"] |
エラー0件 |
("types": [] のとき)
src/n.ts(1,13): 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.
("typeRoots": ["./types"] のとき)
src/n.ts(1,13): error TS2580: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node`.
TypeScript 公式の TSConfig リファレンス(typeRoots の項)には、既定では見えている @types パッケージがすべて取り込まれること、typeRoots を指定するとそこに書いたフォルダ配下のパッケージだけが対象になることが書かれています。typeRoots に自作フォルダだけを書くと node_modules/@types が対象から外れるので、両方を並べます。
もう1つ、typeRoots には「フォルダの形」の決まりがあります。include を ["src"] だけにして、typeRoots に ./typings を指定した状態で比べました。
| 置き方 | 結果 |
|---|---|
typings/legacy-lib/index.d.ts(パッケージ名のフォルダ+index.d.ts) |
エラー0件。--explainFiles に Entry point for implicit type library 'legacy-lib' と表示 |
typings/legacy-lib.d.ts(フォルダ直下にファイルを置く) |
TS7016 のまま |
typeRoots が見るのは「直下のフォルダ」で、直下に置いた d.ts ファイルは読まれません。 自作の d.ts は、typeRoots に頼らず include に入れる方法がわかりやすく、迷いにくいです。
原因9・10:moduleResolution と package.json の exports / types
最近のライブラリは、package.json の exports で公開するファイルを決めています。型定義がどこにあるかも、この exports と moduleResolution の組み合わせで決まります。確認用に、次の package.json を持つ架空のパッケージ modern-lib を用意しました(dist/ には index.js・index.mjs・index.d.ts・utils.js・utils.d.ts があります)。
{
"name": "modern-lib",
"version": "2.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": { "import": "./dist/index.mjs", "require": "./dist/index.js" },
"./utils": { "types": "./dist/utils.d.ts", "default": "./dist/utils.js" }
}
}
// src/m.ts
import { hello } from "modern-lib";
import { sum } from "modern-lib/utils";
console.log(hello(), sum(1, 2));
| 設定 | modern-lib |
modern-lib/utils |
|---|---|---|
| module: commonjs + moduleResolution: node10 | 通る | TS2307 |
| module: node16 | 通る | 通る |
| module: esnext + moduleResolution: bundler | TS7016 | 通る |
(node10 のとき)
src/m.ts(2,21): error TS2307: Cannot find module 'modern-lib/utils' or its corresponding type declarations.
There are types at 'node_modules/modern-lib/dist/utils.d.ts', but this result could not be resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'.
(bundler のとき)
src/m.ts(1,23): error TS7016: Could not find a declaration file for module 'modern-lib'. 'node_modules/modern-lib/dist/index.mjs' implicitly has an 'any' type.
There are types at 'node_modules/modern-lib/dist/index.d.ts', but this result could not be resolved when respecting package.json "exports". The 'modern-lib' library may need to update its package.json or typings.
node10 は exports を読まないため、サブパスの modern-lib/utils を見つけられません。一方、bundler では exports の import 条件が選ばれて index.mjs に進み、その隣の index.d.mts を探しますが、用意されていないので TS7016 になりました。--traceResolution を付けると、この流れがそのまま表示されます(抜粋)。
======== Resolving module 'modern-lib' from 'src/m.ts'. ========
Explicitly specified module resolution kind: 'Bundler'.
Resolving in CJS mode with conditions 'import', 'types'.
Entering conditional exports.
Matched 'exports' condition 'import'.
Using 'exports' subpath '.' with target './dist/index.mjs'.
File name 'node_modules/modern-lib/dist/index.mjs' has a '.mjs' extension - stripping it.
File 'node_modules/modern-lib/dist/index.mts' does not exist.
File 'node_modules/modern-lib/dist/index.d.mts' does not exist.
exports があるパッケージでは、一番外側の types フィールドよりも exports の中の types 条件が使われます。 package.json を次のように直す(types を各条件の先頭に置く)と、bundler でもエラー0件になりました。
{
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" },
"./utils": { "types": "./dist/utils.d.ts", "default": "./dist/utils.js" }
}
}
自分でライブラリを公開するときは、この形を目安にしてください。利用する側で出会った場合は、ライブラリの新しい版を確かめるか、declare module "modern-lib" { ... } を自分の d.ts に書いて補うことができます。
d.tsが読まれているか確かめるコマンド
| コマンド | わかること |
|---|---|
npx tsc -p . --listFiles |
コンパイル対象になったファイルの一覧。自分の d.ts が出てくれば読まれている |
npx tsc -p . --explainFiles |
各ファイルが対象になった理由(include に一致、import された、@types から自動、など) |
npx tsc -p . --traceResolution |
import ごとに、どのファイルをどの順番で探したか |
npx tsc -p . --skipLibCheck false |
d.ts の中のエラー(TS2665、TS2669 など)を表示する |
npx tsc -p . --showConfig |
最終的に使われる設定と、対象ファイルの一覧(files) |
--listFiles と --explainFiles の出力は node_modules 配下も含むので長くなります。grep -v node_modules や grep 自分のファイル名 で絞ると読みやすくなります。
画像やCSSのimportを通すワイルドカード宣言
バンドラー(Vite や webpack など)を使うと、画像や CSS を import できます。TypeScript はこれらの中身を知らないので、宣言が無いと TS2307 になります。
// src/w.ts
import logo from "./logo.svg";
import styles from "./app.module.css";
import "./reset.css";
import text from "./readme.txt?raw";
const url: string = logo;
const cls: string = styles.title;
console.log(url, cls, text);
src/w.ts(1,18): error TS2307: Cannot find module './logo.svg' or its corresponding type declarations.
src/w.ts(2,20): error TS2307: Cannot find module './app.module.css' or its corresponding type declarations.
src/w.ts(4,18): error TS2307: Cannot find module './readme.txt?raw' or its corresponding type declarations.
3行目の import "./reset.css"; は値を受け取らない import なので、エラーになっていません。モジュール名に * を1つ含めた宣言を書くと、パターンに合う import すべてに型が付きます。
// types/assets.d.ts
declare module "*.svg" {
const src: string;
export default src;
}
declare module "*.module.css" {
const classes: { readonly [className: string]: string };
export default classes;
}
declare module "*.css";
declare module "*?raw" {
const content: string;
export default content;
}
この d.ts を置くとエラー0件になりました。型も働いていて、const size: number = logo; を足すと TS2322(Type 'string' is not assignable to type 'number')が表示されます。
| パターン | import の例 | 付く型 |
|---|---|---|
"*.svg"(png・jpg・webp も同じ形) |
import logo from "./logo.svg" |
string(ファイルの URL) |
"*.module.css" |
import styles from "./app.module.css" |
クラス名から文字列への対応表 |
"*.css" |
import "./reset.css" |
any |
"*?raw" |
import text from "./readme.txt?raw" |
string |
import したときに実際に何が入るか(URL か、コンポーネントか)はバンドラーの設定で決まります。ここで確かめたのは tsc の型チェックまでで、バンドラーでのビルドは未実行です。Vite のように型定義を同梱しているツールでは、ツールが案内する方法(vite/client の型を読み込むなど)を先に使い、足りない分だけ自分で宣言すると重複を避けられます。
Window・process.env・Express Requestの拡張テンプレート
既存の型にプロパティを足す、よく使う3つをまとめます。どれも「同じ名前の interface は自動で1つに合わさる」という TypeScript の仕組み(宣言のマージ)を使っています。
| 拡張したいもの | 拡張する相手 | d.ts の形 | 必要なもの |
|---|---|---|---|
window.dataLayer など |
interface Window |
モジュール型(declare global)またはスクリプト型 |
lib に dom |
process.env.API_URL など |
NodeJS.ProcessEnv |
スクリプト型 | @types/node |
req.userId など |
Express.Request |
モジュール型(declare global) |
@types/express |
// types/window.d.ts
export {};
declare global {
interface Window {
dataLayer: unknown[];
appConfig: { apiBase: string };
}
}
// types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: "development" | "production" | "test";
readonly API_URL: string;
readonly PORT?: string;
}
}
// types/express.d.ts
import "express";
declare global {
namespace Express {
interface Request {
userId?: string;
}
}
}
次のコードで、置く前と置いた後を比べました。
// src/e.ts
import express from "express";
const app = express();
app.use((req, _res, next) => {
req.userId = "u-001";
next();
});
app.get("/me", (req, res) => {
res.json({ userId: req.userId });
});
const url: string = process.env.API_URL;
const port: string = process.env.PORT;
const other = process.env.UNKNOWN_KEY;
if (process.env.NODE_ENV === "staging") {
console.log(url, port, other);
}
window.dataLayer.push({ event: "view" });
(d.ts を置く前)
src/e.ts(5,7): error TS2339: Property 'userId' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
src/e.ts(9,26): error TS2339: Property 'userId' does not exist on type 'Request<{}, any, any, ParsedQs, Record<string, any>>'.
src/e.ts(12,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
src/e.ts(13,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
src/e.ts(18,8): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'.
(3つの d.ts を置いた後)
src/e.ts(13,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
src/e.ts(15,5): error TS2367: This comparison appears to be unintentional because the types '"development" | "production" | "test"' and '"staging"' have no overlap.
req.userId・API_URL・window.dataLayer のエラーが消えました。残った2件は、型を付けたからこそ見つかったものです。PORT は省略可能(?)と宣言したので string への代入が指摘され、NODE_ENV は3つの値だけと宣言したので "staging" との比較が指摘されています。宣言していない UNKNOWN_KEY は、これまでどおり string | undefined として使えます。
Express の Request は、express-serve-static-core モジュールを拡張する書き方でも同じ結果になりました。
// types/express.d.ts(別の書き方)
import "express-serve-static-core";
declare module "express-serve-static-core" {
interface Request {
userId?: string;
}
}
ミドルウェアと /me だけにしたコードをポート 8465 で起動し、fetch で呼び出したところ、次の応答が返りました。
200 {"userId":"u-001"}
d.ts は型を足すだけで、実行時の値は用意しません。 req.userId に値を入れるミドルウェアや、環境変数そのものの設定は別に必要です。必ず入るとは限らない値は ? を付けて宣言しておくと、使う側で未設定の場合を考えた書き方に導けます。
自作のJavaScriptに型を付ける/d.tsを自動で作る
プロジェクト内の .js を import したときも、TS7016 になります。
src/c.ts(1,21): error TS7016: Could not find a declaration file for module './lib/calc'. 'src/lib/calc.js' implicitly has an 'any' type.
| 方法 | やること | 結果 |
|---|---|---|
| 隣に d.ts を置く | src/lib/calc.js の隣に calc.d.ts を作る |
エラー0件 |
| allowJs を使う | tsconfig に "allowJs": true |
エラー0件(型は JavaScript から推論される) |
// src/lib/calc.d.ts
export declare function add(a: number, b: number): number;
隣に置く d.ts は、declare module で囲まずに export declare ... をそのまま書きます。ファイル名(拡張子を除いた部分)を .js と同じにするのがポイントです。
反対に、TypeScript で書いたコードからは d.ts を自動で作れます。
npx tsc gen/price.ts --declaration --emitDeclarationOnly --outDir gen/out
// gen/out/price.d.ts(生成されたもの)
export interface Item {
name: string;
price: number;
}
export declare function total(items: Item[], taxRate?: number): number;
元の関数は taxRate = 0.1 という既定値つきの引数でしたが、d.ts では実装が取り除かれ、taxRate?: number という型だけが残っています。d.ts を手で書くのは「TypeScript で書かれていないもの」に型を付けるときで、TypeScript のコードなら tsc に作ってもらうのが確実です。
TypeScript 7.0.2でも確認したこと
2026年10月3日時点で、npm の typescript の latest は 7.0.2 でした(npm view typescript dist-tags で確認)。この記事の主要な点を 7.0.2 の tsc でも実行した結果です。
| 確認したこと | 7.0.2 での結果 |
|---|---|
include が ["src"] だけのとき |
5.9.3 と同じ文面の TS7016 |
include に types を足したとき |
エラー0件 |
"moduleResolution": "node10" を指定 |
TS5108: Option 'moduleResolution=node10' has been removed. |
7.0.2 では node10 の指定そのものが使えなくなっているため、原因9の表の1行目は 5.9.3 だけの結果です。そのほかの検証(原因3〜8、ワイルドカード、拡張テンプレート)は 7.0.2 では未実行です。
よくある質問
Q. d.ts はどこに置けばよいですか?
A. tsconfig.json の include に入っているフォルダならどこでも読み込まれます。types/ や src/types/ にまとめ、include に "types" を足す形がわかりやすいです。ファイル名は自由ですが、legacy-lib.d.ts・assets.d.ts・env.d.ts のように対象ごとに分けると、あとから探しやすくなります。
Q. declare module "x"; の1行と、// @ts-ignore はどちらがよいですか?
A. 1行の declare module のほうが扱いやすいです。import する場所が何か所あっても宣言は1つで済み、あとで波かっこ付きの型に書き換えれば、使っている場所すべてに型が行き渡ります。
Q. skipLibCheck は false にしたほうがよいですか?
A. 普段は true のままで問題ありません。d.ts が効かないときにだけ --skipLibCheck false を付けて実行すると、TS2665 や TS2669 のような d.ts 自体のエラーが表示され、原因の手がかりになります。
Q. エディタでは赤線が出ないのに、tsc ではエラーになります(またはその逆)。
A. エディタは「開いているファイルに一番近い tsconfig.json」を使い、d.ts を追加した直後は前の状態を保持していることがあります。VS Code ならコマンドパレットの「TypeScript: Restart TS Server」で読み直せます(この操作は今回未実行です)。判断の基準は npx tsc -p . の結果に置くと迷いません。
まとめ
- TS7016 は「JavaScript 本体はあるが、型定義が見つからない」というお知らせ。実行時の動作とは別の話で、型を用意すれば解消する
- 直し方は3段階:@types を入れる →
declare module "名前";の1行で通す → 使う関数の型を波かっこの中に書く - d.ts が効かないときは、まず
npx tsc -p . --listFilesで自分の d.ts が読まれているかを確かめ、読まれていなければ include に足す - 一番外側に import / export があると、その d.ts はモジュール扱いになる。グローバルに宣言したいときは
export {};とdeclare global { }を組み合わせる - typeRoots に自作フォルダだけを書くと @types が対象から外れる。自作の d.ts は include で読み込むのがわかりやすい
- exports があるライブラリは、exports の中の
types条件と moduleResolution の組み合わせで型の場所が決まる。--traceResolutionで探した順番を追える - svg・css はワイルドカード宣言、Window・process.env・Express Request は interface の拡張で型を付けられる
参考・出典
- TypeScript 公式 TSConfig リファレンス「typeRoots」 https://www.typescriptlang.org/tsconfig/typeRoots.html (今回開いて確認)
- npm レジストリの typescript パッケージ(
npm view typescript dist-tagsで版を確認)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript 型定義ファイル(d.ts)の書き方とTS7016の直し方」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。