TypeScriptのexportとimportは構文だけならJavaScriptと同じです。つまずくのは、export classが型と値の両方を公開すること、型だけを読み込むときにimport typeを求められること、そしてtsc --initが作る設定がこの2点を厳しく検査することの3つです。この記事では、TypeScript 7.0.2とNode.js 26.5.0で実際にコンパイル・実行した結果(2026年11月2日)をもとに、書き方とエラーの直し方をまとめます。TypeScript自体の型注釈や始め方はTypeScriptとは?型注釈と型推論の書き分け、JavaScriptとの違いと始め方で扱っています。
まとめ:export/importとクラスの要点
- トップレベルに
importかexportがあるファイルはモジュールで、宣言はそのファイルの中に閉じます。tsc --initの設定(moduleDetection: "force")では、どちらも無い実装ファイルもモジュールとして扱われます。ただし、宣言ファイルはこの設定の対象外です。 export classは型と値を同時に公開します。受け取る側はnewにも型注釈にも使えます。interfaceとtypeは型しか公開しません。verbatimModuleSyntaxが有効だと、型だけのものを普通のimportで読むとTS1484になります。import typeかimport { type X }に直します。逆にimport typeで読んだクラスはnewできません(TS1361)。npm init -y(npm 11)はpackage.jsonに"type": "commonjs"を書きます。そこへtsc --initの設定を置くと、export classなどの値宣言にはTS1287、値を読み込むESM形式のimportにはTS1295が出ます。型だけの宣言やimport typeは対象外です。"type": "module"に書き換えれば解消します。- 迷ったら名前付きexportを使います。
export defaultはファイルの主役が1つに決まっているときだけにします。
export/importの基本の書き方
export const・export function・export classの名前付きexport
宣言の前にexportを付けると、その名前で外から読めるようになります。読む側は同じ名前を波かっこで受けます。Node.jsでそのまま動かす前提(module: "nodenext")では、相対パスに拡張子が必要です。以下の例では、出力後の.jsを書きます。rewriteRelativeImportExtensionsを有効にする構成では、.tsを書いて出力時に変換することもできます。
// shapes.ts
export const UNIT = "cm";
export function format(n: number): string {
return n.toFixed(2) + UNIT;
}
export interface Shape {
area(): number;
}
export class Rect implements Shape {
w: number;
h: number;
constructor(w: number, h: number) {
this.w = w;
this.h = h;
}
area() {
return this.w * this.h;
}
}
// main.ts
import { format, Rect, type Shape } from "./shapes.js";
const s: Shape = new Rect(2, 3);
console.log(format(s.area()));
宣言とは別の行でまとめて公開する書き方もあります。export { UNIT, format };のように並べると、ファイル末尾を見るだけで公開範囲がわかります。export { format as formatCm };のようにasで別名にもできます。
よく検索される「export const」とdefaultの組み合わせは書けません。export default const LIMIT = 10;は7.0.2でTS1109(Expression expected)になります。defaultの後ろに置けるのは式と、function・class・interface(TypeScript)などの宣言で、const宣言は置けません。定数を既定の公開にしたいならconst LIMIT = 10; export default LIMIT;と2行に分けます。
export default classと名前付きexportの選び分け
export defaultはファイルに1つだけ置けて、読む側が好きな名前を付けます。
// circle.ts
export default class Circle {
r: number;
constructor(r: number) {
this.r = r;
}
area() {
return Math.PI * this.r ** 2;
}
}
// main.ts
import Circle from "./circle.js";
import MyCircle from "./circle.js";
2行目のように別名で読めてしまうことが、defaultの弱点です。同じクラスがファイルごとに違う名前で呼ばれ、エディタの一括リネームも検索も効きにくくなります。名前付きexportもasで読み込み側の名前を変更できますが、公開名は明示されます。公開名をRecttのように間違えるとTS2724(Did you mean 'Rect'?)でコンパイル時に止まります。
判断はこう決めます。部品を複数公開するファイル、ライブラリの公開口、チームで共有するコードは名前付きexportにします。defaultを使うのは、フレームワークがファイル単位でdefaultを要求する場合(Next.jsのpage.tsxなど)と、ファイルの主役が1つに決まっていて他に何も公開しない場合だけです。両方を混ぜるとimport Circle, { Rect } from ...のように読む側の書き方が揃わなくなります。
再exportによる公開窓口の集約(export * as・export type *)
複数のファイルを1か所から公開する「まとめ口」(index.ts)には再exportを使います。次の4行は7.0.2でエラーなくコンパイルできました。
// index.ts
export * as geo from "./shapes.js";
export type * from "./shapes.js";
export { Rect as Rectangle } from "./shapes.js";
export { default as Circle } from "./circle.js";
export * as geoは名前空間オブジェクトとしてまとめて公開し、読む側はgeo.Rectと書きます。export type *(TypeScript 5.0で追加)は型だけを再公開し、出力されたJavaScriptからは行ごと消えます。defaultを再公開するときは{ default as Circle }と名前を付けます。
まとめ口は便利ですが、バンドラーを通さずに動かすと、値を1つimportするだけでも、出力JavaScriptに残る再export先は読み込み対象になります。型だけの再export先は、この理由では読み込まれません。アプリ内部のコードまでindex.ts経由にする必要はありません。外部に見せる境界にだけ置きます。
export classで公開されるもの(型と値)
宣言ごとに作られる型と値の対応
モジュールとクラスは、どちらも「コードをまとめる単位」に見えますが、役割は重なりません。モジュールはファイル単位のスコープの境界で、クラスはその中に置く宣言の1つです。宣言によって、型を作るのか値を作るのかが決まっています。TypeScriptハンドブックのDeclaration Mergingにある表を整理すると次のとおりです。
| 宣言 | 型 | 値 | newできるか |
|---|---|---|---|
class |
作る | 作る | 抽象クラス・アクセス制限を除き可能 |
interface |
作る | 作らない | できない |
type |
作る | 作らない | できない |
enum |
作る | 作る | できない |
namespace(実装あり) |
名前空間 | 作る | できない |
function・const |
作らない | 作る | 値の構築シグネチャ次第 |
export class Rectと書くと、型のRect(インスタンスの形)と値のRect(コンストラクター関数)が同じ名前で公開されます。型定義ファイルを出力すると、export declare class Rectとして残ります。一方export interface Shapeは出力JavaScriptに何も残りません。interfaceを値として参照するとTS2693、型しか持たないnamespaceを値として参照するとTS2708になります。
クラス型の構造的互換性と#プライベートフィールドによる区別
TypeScriptの型は構造で比べるので、クラス型も例外ではありません。次のbはnewしていないただのオブジェクトですが、Rectと同じプロパティとメソッドを持つので代入できます。
import { Rect } from "./shapes.js";
const b: Rect = { w: 1, h: 1, area: () => 1 };
class Secret {
#key = 1;
get() {
return this.#key;
}
}
const fake: Secret = { get: () => 2 };
bはエラーになりません。fakeは7.0.2でTS2741(Property '#key' is missing)になります。ECMAScriptの#付きプライベートフィールドを持つクラスは、そのクラスから作ったインスタンスしか受け付けなくなります。「このクラスの実体だけを受け取りたい」なら#フィールドを1つ持たせ、形だけ合えばよいならinterfaceで受けるのが筋です。
import typeが必要になる条件(verbatimModuleSyntax)
interfaceを普通にimportするとTS1484になる理由
verbatimModuleSyntaxは、typeの付いたimportだけを出力から消し、付いていないimportはそのまま残すオプションです。TypeScript 5.9以降のtsc --initは、この設定をtrueにしたtsconfig.jsonを作ります(TypeScript 5.9のリリースノート)。7.0.2・6.0.3・5.9.3の--initはどれもverbatimModuleSyntax: true・moduleDetection: "force"・module: "nodenext"を書き出しました。
この設定でimport { Shape, Rect } from "./shapes.js"と書くと、型しかないShapeにTS1484が出ます。TS1484を放置したまま出力したJavaScriptにはimport { Shape, Rect }がそのまま残り、Node.jsで実行するとSyntaxError: The requested module './shapes.js' does not provide an export named 'Shape'で止まりました。tscはエラーがあっても既定では出力を止めず、今回の検証ではoutDirに指定したdistへ書き出すので、終了コードを見ずに実行すると実行時に初めて気づきます。直し方は2通りです。
import type { Shape } from "./shapes.js";
import { Rect } from "./shapes.js";
// または、上の2行の代わりに次の1行を使います。
// import { type Shape, Rect } from "./shapes.js";
1つのファイルから型と値を両方読むなら、下の{ type Shape, Rect }の方が1行で済みます。export側も同じで、型だけを再公開するときはexport type { Shape }と書きます。
import typeで読み込んだクラスの値利用制限(TS1361)
クラスは値でもあるので、import type { Rect }で読むと型注釈には使えますがnew Rect(2, 3)はTS1361('Rect' cannot be used as a value because it was imported using 'import type')になります。インスタンスを作るファイルではtypeを付けずに読みます。引数や戻り値の型にだけ使うファイルならimport typeで十分で、そのファイルの出力からはimport行が消えます。
通常のimportとimport typeは、読み込んだ名前を実行時に使うかどうかで選びます。new・関数呼び出し・instanceofに使うならimport、型の位置にしか書かないならimport typeです。
tsc --initの設定で起きるモジュールのエラー
ファイルのモジュール判定(moduleDetectionとexport {})
TypeScriptハンドブックのModulesは、トップレベルのimport・exportを含むファイルをモジュール、含まないファイルをスクリプトと定義しています。スクリプトの宣言はグローバルに置かれるので、別ファイルの同名変数とぶつかります。
importもexportも無いa.tsとb.tsの両方にconst count = 1;を書き、moduleDetectionもmoduleも指定しない最小のtsconfig.jsonでコンパイルすると、7.0.2でも6.0.3でもTS2451(Cannot redeclare block-scoped variable 'count')が両方のファイルに出ました。moduleDetection: "force"を指定するとエラーは0件になります。既定のautoでも、module: "nodenext"のもとでpackage.jsonが"type": "module"なら、importやexportの無いファイルもモジュールとして扱われます(moduleDetectionの説明)。tsc --initの設定はこのforceを含むので、新しく作ったプロジェクトではこの衝突は起きません。古いtsconfig.jsonのままの場合は、ファイル末尾にexport {};を1行足してもモジュール扱いになります。何も公開しないexportですが、モジュールにする目的で使われる定番の書き方です。
nodenextのESM相対importに必要な拡張子(TS2835)
module: "nodenext"のもとでESMとして扱われるファイルでは、import { Rect } from "./shapes"がTS2835(Relative import paths need explicit file extensions ... Did you mean './shapes.js'?)になります。Node.jsのESMは拡張子を補わないので、TypeScriptも出力後のパスを書かせます。
Vite・esbuild・webpackなどのバンドラーが解決するプロジェクトなら、module: "preserve"(moduleResolutionはbundlerになります)にすると拡張子なしで書けます。Node.jsで直接動かすのか、バンドラーを通すのかで設定を選び、混ぜないことが大事です。
CommonJSとverbatimModuleSyntaxによるTS1287・TS1295
npm 11.17.0のnpm init -yは、package.jsonに"type": "commonjs"を書き出します。そこにtsc --initで作った設定を置き、普通にexport classとimportを書くと、7.0.2で次の2つが出ました。
export class Counterの行:TS1287(A top-level 'export' modifier cannot be used on value declarations in a CommonJS module when 'verbatimModuleSyntax' is enabled.)import { Counter }の行:TS1295(ECMAScript imports and exports cannot be written in a CommonJS file under 'verbatimModuleSyntax'.)
package.jsonにtypeが無い場合も、CommonJS扱いになるので同じエラーです。ESMで書くつもりなら"type": "module"に書き換えます。2つのエラーはそれで消えます。CommonJSのまま書くなら、後述のexport =とimport ... = require()を使います。ESMとCommonJSの読み込みの違いやNode.jsでの相互運用はESMとCommonJSの違い|require(esm)で変わった相互運用と移行手順【Node.js 26実測】で詳しく扱っています。
Node.jsで.tsを直接実行するときのクラスとimportの制約
Node.jsは型注釈を取り除くだけで.tsを実行できます(型の除去は v23.6.0・v22.18.0で既定有効、v25.2.0・v24.12.0で安定版)。変換が要る構文は扱わず、Node.jsのTypeScriptドキュメントはenum、実行時のコードを含むnamespace、パラメータプロパティ、importエイリアスを未対応の代表に挙げています。Node.js 26.5.0で試すと、モジュールとクラスまわりで次の3点に当たりました。
constructor(public w: number, public h: number) {}のパラメータプロパティを持つクラスを読むと、ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(TypeScript parameter property is not supported in strip-only mode)で止まります。フィールドを宣言してコンストラクター内で代入する書き方に直すと動きました。typeを付けずにinterfaceを読むと、実行時にdoes not provide an export named 'Shape'になります。型の除去は読み込み先のファイルを見ずに注釈を消すだけなので、typeの無いimportは値の読み込みとして残ります。import typeが必須です。importのパスは.tsまで書きます。型検査だけを行うならnoEmit: trueとallowImportingTsExtensions: trueを指定します(無いとTS5097)。tscで出力もするならrewriteRelativeImportExtensionsを付けます。.tsのまま書けて、出力側のパスは.jsに書き換わります。
このうちパラメータプロパティなど、JavaScriptへの変換が必要な構文はerasableSyntaxOnly(TypeScript 5.8で追加)で検出でき、パラメータプロパティはTS1294(This syntax is not allowed when 'erasableSyntaxOnly' is enabled.)になりました。typeの付け忘れはこのオプションでは見つからないので、verbatimModuleSyntaxと併用します。Node.jsでそのまま動かすコードを書くなら、最初から両方を入れておきます。
CommonJSのexport =と7.0で削除されたモジュール設定
export =とimport = requireの書き方
CommonJSとして出力するファイルで、module.exportsにクラスを丸ごと入れたい場合はTypeScript独自のexport =を使います。読む側はimport ... = require()で受けます。
// counter.ts("type": "commonjs")
class Counter {
n = 0;
inc() {
return ++this.n;
}
}
export = Counter;
// main.ts
import Counter = require("./counter.js");
console.log(new Counter().inc());
この組み合わせはverbatimModuleSyntaxが有効でもエラーにならず、module.exports = Counter;が出力されて実行結果は1でした。6.0以前の記事ではexport =のモジュールをimport Counter fromで読むためにesModuleInteropを有効にする方法がよく紹介されますが、7.0ではesModuleInterop: falseとallowSyntheticDefaultImports: falseの指定自体が削除され、どちらも常に有効です。
7.0で削除されたmodule・moduleResolution・baseUrlの移行先
2026年7月8日に公開されたTypeScript 7.0の告知は、次の設定を削除済みとしています。7.0.2では該当する値を書くとTS5108などで止まります。
| 削除された設定 | 移行先 |
|---|---|
module: amd / umd / system / none |
esnextかpreserve+バンドラー |
moduleResolution: node / node10 |
nodenextかbundler |
moduleResolution: classic |
nodenextかbundler |
baseUrl |
pathsをプロジェクト基準の相対パスで書く |
esModuleInterop: false |
指定を消す(常に有効) |
告知の文面は「systemjs」ですが、tsconfig.jsonに書く実際の値はsystemです。自分の設定を探すときは"module": "system"で検索します。baseUrlを外してpathsを書き直す手順はtsconfigのbaseUrl非推奨(TS5101)の直し方とignoreDeprecations・TypeScript 7.0の削除エラー、6.0を経由する移行全体はTypeScript 6.0とは|5から6への移行手順とts5to6・7.0との違い【6.0.3対応】、7.0そのものの変更点はTypeScript 7(TS7)とは?Go製tscで約10倍速く|7.0.2への移行とtsgo・7.1 betaの現状にまとめています。
循環importで起きるReferenceError(const・classとfunctionの差)
a.tsとb.tsが互いをimportし、a.tsが読み込まれた直後にbを呼ぶ構成を作りました。エントリーをb.jsにすると、先にa.jsが評価され、その時点でb.jsの本体はまだ実行されていません。
// a.ts
import { b } from "./b.js";
export const a = () => "a";
console.log(b());
// b.ts
import { a } from "./a.js";
export function b() {
return "b";
}
この形はbと表示されて成功しました。function宣言はモジュールのリンク時に初期化されるからです。b.tsをexport const b = () => "b";にするとReferenceError: Cannot access 'b' before initialization、export class Bにしてnew B()を呼ぶとCannot access 'B' before initializationになりました。クラス宣言はconstと同じく、宣言の行が実行されるまで使えません。
TypeScriptはこの循環を型検査では検出しません。循環をなくすのが根本の対処で、両方が使う定数や型を3つ目のファイルに出すと、多くの場合は解消します。
クラスを作るべきでない場面とモジュール分割の判断
モジュールがファイル単位で名前を閉じてくれる以上、「関連する関数をまとめるためだけのクラス」は要りません。staticメソッドしか持たないクラス(class StringUtil { static trim() {} }のような形)は、関数を名前付きexportに並べたモジュールに置き換えます。インスタンスを作らないのでnewの意味が無く、使わない関数もStringUtilごと読み込むことになります。
クラスにする価値があるのは、状態(フィールド)とそれを変える操作をひとまとめにしたいとき、instanceofで種類を判定したいとき、#フィールドで外から触れない値を持ちたいときです。データの形を決めたいだけならinterfaceかtypeで足ります。値オブジェクトや集約をクラスで組む実装例はDDDの実装手順:値オブジェクトから集約まで既存コードを段階移行するTypeScript実例にあります。
モジュールの分け方は、一緒に変更されるものを同じファイルに置くのが基本です。ファイルを分けた結果、値の相互importが生じ、初期化順序への依存や変更の連鎖が増えたら、分割を見直します。型だけの相互参照は実行時の循環依存にはなりません。前の章の循環importの形になる前に、共有部分を切り出します。
よくある質問
export interfaceとexport typeの違いは何ですか?
どちらも型だけを公開し、出力されるJavaScriptには何も残りません。違いは宣言の性質です。interfaceは同じ名前で2回書くと中身が合わさります(宣言のマージ)が、片方だけexportするとTS2395になります。typeは同名で再宣言できず、ユニオン型や条件型などinterfaceで書けない型も表せます。オブジェクトの形を公開するならどちらでも構いません。
importのパスに.js拡張子は必要ですか?
module: "nodenext"(またはnode16)でESMとして扱われるファイルでは必要で、書かないとTS2835です。通常のJavaScript出力では.jsを書きます。rewriteRelativeImportExtensionsを使う構成やNode.jsで直接実行する構成では、.tsを指定できます。module: "preserve"でバンドラーが解決する構成なら省略できます。
namespaceはモジュールの代わりに使えますか?
新しいコードでは使いません。ファイル単位のモジュールで名前は閉じられるので、namespaceを重ねる理由がありません。実行時のコードを含むnamespaceはNode.jsの型の除去でも動きません。古いmodule Foo {}という書き方は、6.0.3でも7.0.2でもTS1540(A 'namespace' declaration should not be declared using the 'module' keyword.)になります。namespaceキーワードに置き換えておきます。
モジュールとクラスは同じファイルに書いてよいですか?
書けますし、普通の書き方です。ファイルがモジュールで、その中でexport classを宣言する形になります。1ファイル1クラスにする決まりはTypeScriptにはありません。一緒に変更される小さなクラスや補助関数は同じファイルにまとめ、大きく育ったら分けます。
型定義ファイル(.d.ts)もモジュールとして扱われますか?
モジュールにもグローバルな宣言ファイルにもなります。通常の.d.tsにはmoduleDetection: "force"が適用されず、トップレベルにimportかexportがあればモジュール、無ければグローバルな宣言になります。グローバルな型を足すつもりで書いた.d.tsにimportを1行足すと、その宣言はモジュールの中に閉じてしまい、他のファイルから見えなくなります。その場合はdeclare global { }で囲みます。