Menu

TypeScriptのモジュール: import・export と import type

トップレベルに import か export があるTypeScriptのファイルはすべてモジュールです。名前付きエクスポートとデフォルトエクスポート、import type と export type、module 設定で ES モジュールと CommonJS のどちらが出力されるか、node16 と nodenext で import に .js 拡張子が必要な理由を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

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" のあるパッケージの .tsES モジュール
それがないパッケージの .tsCommonJS
.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/... パッケージをインストールするか、小さな宣言ファイルを書きます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める