TypeScriptのモジュールとは、トップレベルに import か export が少なくともひとつあるファイルです。その中で宣言したものは export しない限りファイルの中だけで使え、ほかのファイルは必要なものを import します。構文はJavaScriptの ES モジュールの構文に、型専用の書き方がいくつか加わったものです。
名前付きエクスポートとデフォルトエクスポート
プロジェクトには多くのファイルがあるので、import する側は次のようになります。デフォルトエクスポートは波かっこなしで import し、好きな名前を付けられます。名前付きエクスポートは波かっこの中に書き、as で名前を変えない限りそのままの名前で使います。
// main.ts
import describe, { distance, ORIGIN, type Point } from "./math.js";
import { distance as dist } from "./math.js"; // renamed on import
import * as math from "./math.js"; // everything, as one object
const p: Point = { x: 6, y: 8 };
console.log(describe(p), distance(ORIGIN, p), dist(p, p), math.ORIGIN);
| 書き方 | import されるもの |
|---|---|
import { a, b } from "./m.js" | 名前付きエクスポート a と b |
import x from "./m.js" | デフォルトエクスポートを x という名前で |
import * as m from "./m.js" | すべてのエクスポートを持つ名前空間オブジェクト |
import { a as b } from "./m.js" | a をこのファイルでは b という名前で |
import type { T } from "./m.js" | 型だけ。出力からは取り除かれる |
import "./setup.js" | 副作用のためにファイルを実行する |
export { a } from "./m.js" | a を import せずに再エクスポートする |
export * from "./m.js" | すべての名前付きエクスポートを再エクスポートする |
再エクスポートを使うと、ひとつのファイル(多くは index.ts)にフォルダーの公開 API をまとめられます。ライブバインディングやモジュールのキャッシュといったJavaScriptのモジュール自体の動作は、ES モジュールで説明しています。
import type と export type
型は実行時には存在しないので、型としてしか使わない import には読み込むものがありません。import type はそれを明示し、その文はJavaScriptの出力から消えます。type 修飾子は、普通の import の中の個々の名前にも付けられます。
import type { User } from "./models.js"; // whole statement erased
import { saveUser, type Settings } from "./api.js"; // only saveUser survives
export type { User }; // re-export a type only
export type UserId = User["id"];
キーワードがなくても、TypeScriptは型だけだとわかる名前を取り除きます。次の2つの設定では、このキーワードが必須になります:
verbatimModuleSyntax: trueはtypeの付いていない import をすべて残すので、印のない型の import はエラー TS1484'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.になります。- Node で
.tsファイルを直接実行する(型除去)と、型注釈は取り除かれますが、ほかのファイルは見ません。印のないimport { Point }はコードに残り、Node は実行時にSyntaxError: The requested module './math.ts' does not provide an export named 'Point'で失敗します。
型だけの import にはいつも type を書くようにすれば、どちらの場合でも動くので、習慣にしておきましょう。
Cannot Find Module
import のパスがファイルや型付きのパッケージにたどり着かないと、コンパイラーはエラー TS2307 で止まります。次のブロックを実行して確認してください:
出力は index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. です。よくある原因:
- 相対パスのタイプミスや、
./の書き忘れ(なければ名前はnode_modulesから探されます)。 - 型を同梱していないJavaScriptのパッケージ:
@types/{package}があればインストールし、なければ宣言ファイルを書きます。 - パッケージの
package.jsonのexportsフィールドに載っていないサブパス。node16、nodenext、bundlerの解決方式では、載っているエントリーポイントしか import できません。
ES モジュールと CommonJS の出力
書くのは常に import と export です。どんなJavaScriptが出力されるかは、tsconfig.json の module オプションで決まります。
// Source, the same in both cases
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package with "type": "module"
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package without "type": "module"
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_js_1 = require("./math.js");
console.log((0, math_js_1.add)(1, 2));
node16、node18、node20、nodenext では、TypeScriptはファイルごとに Node 自身のルールに従います:
| ファイル | 出力形式 |
|---|---|
"type": "module" のあるパッケージの .ts | ES モジュール |
それがないパッケージの .ts | CommonJS |
.mts | 常に ES モジュール。.mjs として出力 |
.cts | 常に CommonJS。.cjs として出力 |
module: "esnext" や "preserve" では、出力に import/export がそのまま残ります。Vite や esbuild のようなバンドラーが期待するのはこの形です。2つのモジュールシステムの実行時の違いは CommonJS と ESM で説明しています。
import の拡張子
module: node16 や nodenext では、ES モジュールのファイルは Node が実際に読み込むファイルを指定しなければならず、それはコンパイル後の .js ファイルです。型チェックのときには、TypeScriptは ./math.js を math.ts に対応させます。
import { add } from "./math"; // error TS2835 in an ES module file
import { add } from "./math.js"; // correct: the path as it exists after compiling
エラーの文面は Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? です。同じ設定でも CommonJS のファイルでは拡張子を省略できます。require が自分で拡張子を試すからです。
ルールが変わる設定がほかに2つあります:
moduleResolution: "bundler"(moduleがesnext、preserve、commonjsのいずれか)では、バンドラーが解決するので拡張子なしの./mathを受け付けます。rewriteRelativeImportExtensions: trueを使うと、Node が.tsファイルを直接実行するときにも通るパス./math.tsと書けて、出力では./math.jsに書き換えられます。
モジュール解決と paths
"zod" のような裸の名前の場合、TypeScriptは node_modules を探し、パッケージの package.json(exports と types のフィールド)を読み、見つからなければ node_modules/@types/zod を使います。相対の名前(./、../)は import するファイルの位置から解決されます。.json ファイルも import できます。そのための設定は JSON のページにあります。
tsconfig.json の paths で、自分のフォルダーにエイリアスを追加できます:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@lib/*": ["./src/lib/*"]
}
}
}
paths が影響するのは型チェックだけです。出力されるファイルには import { v } from "@lib/util" のまま残るので、実行時にはほかの何かが解決しなければなりません。同じエイリアスを設定したバンドラーか、package.json の Node の imports フィールド("#lib/*": "./dist/lib/*" のように # で始まるエイリアス)です。後者は追加のツールなしで動きます。baseUrl は TypeScript 7 で廃止されました(エラー TS5102)。paths のエントリーは、tsconfig.json からの相対パスとして ./ から書きましょう。
よくある質問
TypeScriptの import と import type の違いは何ですか?
import type { User } from "./user.js" は型しか取り込めず、文全体がJavaScriptの出力から取り除かれます。普通の import は値も型も取り込めます。TypeScriptは型としてしか使われない名前を取り除きますが、verbatimModuleSyntax を有効にすると、出力が書いたとおりになるようにそれらに type を付けることが必須になります。
TypeScriptの import パスに .js が必要なのはなぜですか?
module: node16 や nodenext では、ES モジュールのファイルは実行時に Node が実際に読み込むファイル名で import しなければならず、そのファイルはコンパイル後の .js です。型チェックのときには、TypeScriptは ./math.js を math.ts に対応させます。ES モジュールのファイルで拡張子を省くとエラー TS2835 になります。
TypeScriptでは export default と名前付きエクスポートのどちらを使うべきですか?
どちらも使えます。名前付きエクスポートを好むチームが多いです。import するすべてのファイルで名前が同じになり、エディターの自動 import が確実に働き、名前の変更が検索ではなくリファクタリングで済むからです。デフォルトエクスポートでは、import する側がそれぞれ好きな名前を付けられます。
tsconfig の paths オプションは出力の import を変えますか?
変えません。paths は型チェッカーにモジュールの場所を教えるだけです。出力されるJavaScriptには "@lib/util" が書いたまま残るので、実行時にはバンドラーか、package.json の Node の imports フィールドで解決する必要があります。
TypeScriptの「Cannot find module」を直すには?
エラー TS2307 は、パスがTypeScriptの見つけられるファイルを指していないか、パッケージが型を同梱していないことを意味します。相対パスと拡張子を確認し、組み込みの型がないパッケージなら @types/... パッケージをインストールするか、小さな宣言ファイルを書きます。