TypeScript でコードを書いていると、「この import は型を使うためだけに書いているのに、コンパイル後の JavaScript にも残ってしまうのだろうか」と気になることがあります。そんなときに使うのが import type と export type です。これらは「型情報だけ」をやり取りするための構文で、コンパイル後の JavaScript からは完全に消えます。この記事では、import type / export type の基本的な書き方から、型と値を1文にまとめるインライン形式、なぜこの構文が必要になるのか、通常の import との違い、そしてつまずきやすいポイントまでを、具体的なコードとあわせて解説します。
目次
import type が「型だけ」を運ぶという意味
TypeScript のモジュールには、「値」と「型」という2種類のものが含まれています。関数や変数、クラスの実体は「値」で、コンパイル後の JavaScript にも残ります。一方、interface や type で定義した型注釈は「型」で、TypeScript がチェックに使うだけの情報です。型は実行時には存在しないため、コンパイルされると跡形もなく消えます。
import type は、この「型だけ」をインポートすることを明示する構文です。次のように書くと、User は型としてのみ使えるインポートになり、コンパイル後の JavaScript にはこの import 文自体が残りません。
// 型を定義してエクスポートする
export interface User {
id: number;
name: string;
}
// 型としてだけ User を読み込む
import type { User } from './types';
const user: User = {
id: 1,
name: 'Taro',
};
この main.ts をコンパイルすると、import type { User } from './types'; の行はまるごと削除されます。User は型注釈として const user: User の部分でしか使われておらず、実行時には不要だからです。./types というモジュールへの依存も JavaScript には残りません。
export type で型だけを再エクスポートする
エクスポート側にも同じ考え方があります。export type を使うと、その名前が「型としてのみ」エクスポートされることを明示できます。すでにインポート済みの型を別のモジュールから再エクスポート(re-export)するときによく使います。
import type { User } from './types';
// 型 User を、このモジュールからも型として公開する
export type { User };
// 別モジュールの型をまとめて再エクスポートすることもできる
export type { Product } from './product';
export type { User }; と書くことで、User は型としてだけ外部に公開されます。値としては存在しないので、これをインポートした側が誤って値として使おうとすればコンパイルエラーになり、意図しない使い方を防げます。型定義をまとめた「バレルファイル(barrel file)」と呼ばれる index.ts で、複数の型をまとめて公開する場面で重宝します。
型と値を1文で混在させるインライン形式
1つのモジュールから、型と値の両方をインポートしたいことはよくあります。このとき、型のインポートだけを別の import type 文に分けると行数が増えて煩雑です。そこで用意されているのが、import 文の中で名前ごとに type を付けられるインライン形式です。
// createUser は値(関数)、User は型
// 型側の名前にだけ type を付ける
import { type User, createUser } from './module';
const user: User = createUser('Taro');
この書き方では、User の前にだけ type を付けています。こうすると User は型としてのみ扱われ、コンパイル後には削除されます。一方 createUser は通常のインポートなので、値として JavaScript に残ります。1つの import 文で「消える型」と「残る値」を混在させられるため、余計な行を増やさずに済みます。エクスポート側でも export { type User, createUser }; のように同じ書き方が使えます。
なぜ import type が必要になるのか
「型注釈にしか使っていないなら、TypeScript が勝手に import を消してくれればいいのでは」と思うかもしれません。実際、昔ながらの設定では TypeScript がある程度その判断をしてくれます。しかし、いくつかの理由から import type を明示することが推奨される、あるいは必須になる場面があります。
副作用のあるモジュールを誤って実行しないため
モジュールの中には、読み込まれただけで何らかの処理(副作用)を実行するものがあります。import type を使うと「型だけが必要」という意図がはっきりするため、型のためにモジュールを読み込んだつもりが、実際にはそのモジュールのコードが実行されてしまう、という事故を防げます。型情報だけを参照したい相手には import type を使うのが安全です。
バンドラやトランスパイラが型を確実に消せるため
近年は Babel や esbuild、swc といったツールが、1ファイルずつ独立して変換(トランスパイル)を行います。これらのツールは型情報を完全には把握できないため、「この import は型だけだから消してよい」という判断を安全に下せないことがあります。import type と明示されていれば、そのツールは中身を解析しなくても「これは削除してよい import だ」と確実に判断でき、無駄なインポートをバンドルから取り除けます。
isolatedModules や verbatimModuleSyntax の環境で必要になるため
tsconfig.json で isolatedModules や verbatimModuleSyntax を有効にしている場合、型と値の区別を構文として明示することが求められます。特に verbatimModuleSyntax は、書いた import / export をそのまま出力に反映する設定で、型としてしか使わない名前は import type で書かないとエラーになったり、逆に不要な import が JavaScript に残ってしまったりします。こうしたモダンな設定では、import type を正しく使い分けることが前提になります。
通常の import との違いを整理する
ここまで見てきた import type と通常の import の違いを、表で整理しておきます。「コンパイル後に残るかどうか」と「値として使えるかどうか」が大きな分かれ目です。
| 項目 | import(通常) | import type |
|---|---|---|
| 運ぶもの | 値と型の両方 | 型情報だけ |
| コンパイル後の JS | 残る(値として使う場合) | 完全に消える |
| 値としての利用 | できる | できない(エラー) |
| モジュールの副作用 | 実行される | 実行されない |
| 主な用途 | 関数・変数・クラスの実体 | interface・type などの型注釈 |
ざっくり言えば、実行時に実体が必要なものは通常の import、型チェックのためだけに名前を借りたいものは import type、と覚えておくとよいでしょう。
import type したものを値として使うとエラーになる
最もよくあるつまずきが、import type で読み込んだ名前を値として使ってしまうケースです。型としてインポートしたものは実行時には存在しないため、値の位置で使おうとするとコンパイルエラーになります。
import type { createUser } from './module';
// エラー: 'createUser' は型としてインポートされているため
// 値として使えない
const user = createUser('Taro');
この場合、createUser は関数(値)なので、import type ではなく通常の import で読み込む必要があります。型として使うものと値として使うものを取り違えないよう注意しましょう。型と値を同時に扱いたいときは、前述のインライン形式 import { type User, createUser } のように、名前ごとに type を付けるかどうかを分けるのがおすすめです。
クラスは型と値の両方を持つので使い分けが要る
クラスは少しややこしい存在です。クラス名は「型」としても「値」としても使えるからです。型注釈の位置で使えばインスタンスの型を表し、new で呼べば実体を生成する値として振る舞います。そのため、クラスを import type で読み込むと、型注釈には使えても new できなくなります。
// 型としてだけ使うなら import type でよい
import type { UserModel } from './user-model';
// 型注釈には使える
function printUser(user: UserModel): void {
console.log(user);
}
// ただし new はできない(値が必要なため)
// const u = new UserModel(); // ← エラーになる
クラスを型注釈にしか使わないなら import type で構いませんが、new してインスタンスを作る場合は通常の import が必要です。「そのクラスを実行時に呼び出すのか、それとも型の形として参照するだけなのか」で使い分けるのがポイントです。判断に迷ったら、まず通常の import で書いておき、型としてしか使っていないと分かった時点で import type に変える、という進め方でも問題ありません。
まとめ
import type と export type は、型情報だけをやり取りするための構文で、コンパイル後の JavaScript からは完全に消えます。基本形は import type { User } from './types'; と export type { User }; で、型と値を1文で混在させたいときは import { type User, createUser } のようにインライン形式で名前ごとに type を付けます。この構文は、副作用のあるモジュールを誤って実行しないため、バンドラやトランスパイラが型を確実に削除できるようにするため、そして isolatedModules や verbatimModuleSyntax といったモダンな設定に対応するために役立ちます。注意点として、import type したものは値として使えずエラーになること、クラスは型と値の両方を持つため new するなら通常の import が必要なことを押さえておきましょう。