TypeScript モジュール

JavaScript(TypeScript)には ES モジュールと CommonJS の2つのモジュールシステムがありますが、以下は TypeScript での ES モジュール(export や import)の使い方や型のエクスポート・インポート、Type-only imports/exports、モジュールとスクリプトのスコープなどについての覚書です。

作成日:2023年11月21日

関連ページ

参考サイト

JavaScript 参考サイト

ES モジュール(ESM)

ES モジュール(ES Modules)は ECMAScript モジュールや略して ESM などとも呼ばれ、主にフロントエンド(ブラウザ)で利用されるファイルの読み込み方法です。

ES モジュールは、export 文(宣言)を使用して変数や関数などをエクスポートして、別のモジュールからimport 文(宣言)を使ってエクスポートされた変数や関数などをインポートして利用できます。

TypeScript や JavaScript においては1つのファイルが1つのモジュールとなります。

例えば、以下のようなファイル構成がある場合、

tsp // プロジェクト
├── dist
│   ├── index.js // コンパイルされたモジュール(エントリーポイント)
│   └── module.js // コンパイルされたモジュール
├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── index.ts // モジュール(エントリーポイント)
│   └── module.ts // モジュール
└── tsconfig.json

以下の module.ts は export を使って value という変数をエクスポートするモジュールになります。

TypeScript では、コード中に importexport が使われている場合は自動的にモジュールとして扱われます。この例の module.ts と index.ts はモジュールとして扱われます(スクリプトとモジュール)。

module.ts
export const value = 'abc';

index.ts で module.ts から value をインポートするには以下のように import を使います。

index.ts
import { value } from "./module.js";  // 拡張子は .js
console.log(value);  // abc と出力される

これでコンパイルを実行して、 node コマンドにコンパイルされた dist/index.js を指定して実行すると abc と出力され、module.ts で定義した変数 value を index.ts で使用できていることが確認できます。

% npx tsc  // コンパイルを実行
% node dist/index.js  // node コマンドを実行
abc

tsconfig.json

この例での tsconfig.json です。環境に応じて変更します。

{
  "compilerOptions": {
    "target": "es2022",
    "module": "esnext",
    "moduleResolution": "node10",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true
  },
  "include": ["./src/**/*.ts"],
}

関連項目:tsconfig.json

package.json

この例での package.json です。環境に応じて変更します。

{
  "name": "tsp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "watch": "tsc --watch",
    "build:live": "tsc --watch --noEmitOnError"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^20.6.0",
    "typescript": "^5.2.2"
  }
}

関連項目:プロジェクトを ES Modules に

ブラウザで確認

ブラウザ(index.html)で確認するには、ローカル環境が必要になります。

直接 HTML ファイル(index.html)を開くこともできますが、その場合 file:/// からはじまる URL になり same Origin Policy により、JavaScript モジュールが正しく動作しません。

また、コンパイルされて出力された JavaScript ファイル(dist/index.js)は import 文を使っているのでモジュールとして扱う必要がるため、HTML からは type="module" を指定して読み込みます。

この例の場合、以下のように dist/index.js を読み込めば、ローカル環境で index.html を開いてコンソールタブで出力を確認できます。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TypeScript Sample</title>
</head>
<body>
  <h1>TypeScript Sample</h1>
</body>
<!-- type="module" を指定してコンパイルされた JavaScript モジュールを読み込む -->
<script src="dist/index.js" type="module"></script>
</html>

エクスポートとインポート

エクスポートとインポートには「名前つき」と「デフォルト」という2種類の方法があります。

種類 説明
名前付きエクスポート モジュールごとに複数のエクスポートを行うエクスポート
名前付きインポート モジュールから名前を指定して選択的に行うインポート
デフォルトエクスポート モジュールごとに1つしかエクスポートできないエクスポート。名前付きエクスポートと併用可能
デフォルトインポート モジュールのデフォルトエクスポートに名前をつけて行うインポート

名前付きエクスポートや名前付きインポートは、単にエクスポートやインポートと呼ぶこともあります。

名前付きエクスポート/インポート

export

名前付きエクスポートを使うと、モジュールごとに複数の機能(変数や関数、クラスなど)をエクスポートすることができます。

モジュール内のトップレベル(ブロックや関数などの中以外)での変数や関数などの宣言(const や let、function)の前に export を記述することで宣言と同時にそれぞれの機能を個別にエクスポートできます。

以下はファイル module.ts で、変数と関数を宣言と同時に(名前付き)エクスポートする例です。ファイル module.ts は export を使っているのでモジュールとして扱われます。

module.ts(宣言と同時にエクスポート)
//変数をエクスポート
export const name = "foo";
export const age = 22;

//関数をエクスポート(アロー関数)
export const hello = () => console.log("Hello");

//関数をエクスポート(関数宣言)
export function greeting() {
  console.log(`My name is ${name} and ${age} years old`);
}

宣言とは別にエクスポート

宣言とは別にまとめてエクスポートすることもできます。

すでに定義済みの機能を以下のような書式でモジュールファイルの末尾に単一の export を使ってエクスポートすることができます。

export { 変数のカンマ区切りリスト };

以下は、前述の例を書き換えたものです(内容的には同じです)。

module.ts(宣言とは別にエクスポート)
//変数を定義
const name = "foo";
const age = 22;

//関数を定義(アロー関数)
const hello = () => console.log("Hello");

//関数を定義(関数宣言)
function greeting() {
  console.log(`My name is ${name} and ${age} years old`);
}

//定義済みの機能を export 文でリストで指定してエクスポート
export { name, age, hello, greeting };
import

名前付きインポートを使うと、エクスポートされた機能の名前を指定してモジュールから選択的にインポートすることができます。

import 宣言はモジュールのトップレベル(ブロックや関数などの中以外)にのみ書くことができます。

書式は、以下のように import 文に続けて{}内にインポートしたい機能(変数)のリストをカンマ区切りで指定し、from でモジュールファイルへのパスを指定します。

import { 変数のカンマ区切りリスト } from "モジュールのパス";

以下は前述の module.ts でエクスポートしたデータ(変数 name, age と関数 hello, greeting)を同じディレクトリにある index.ts で import を使ってインポートする例です。

モジュールのパスはカレントディレクトリを表す ./ や1つ上のディレクトリを表す ../ を使って指定します。同じディレクトリにある場合でも、./は省略できません。

モジュールパスのファイル名はコンパイル後の拡張子.jsを指定します。モジュールバンドラを使用している場合などでは拡張子を省略できます(バンドラやその設定により異なります)。

また、npm でインストールしたモジュール(node_modules 内のモジュール)はパスや拡張子を省略してモジュール名だけで参照できます。

index.ts
import { name, age, hello, greeting } from "./module.js";

// インポートしたデータを使用
console.log(name, age); // foo 22
hello(); // Hello
greeting(); // My name is foo and 22 years old

これで index.ts をコンパイルして、node コマンドにコンパイル後の dist/index.js を指定して実行すると以下のように表示されます。

% npx tsc  // コンパイルを実行
% node dist/index.js // node コマンドを実行
foo 22
Hello
My name is foo and 22 years old

インポートされたモジュールは実行される

インポートされたモジュール(ファイル)はそのコードが実行されます。その際、インポートされる側のモジュールが、インポートする側のモジュールよりも先に実行されます。

例えば、以下のように module.ts と index.ts を書き換えて、

module.ts(インポートされる側のモジュール)
export const hello = () => console.log("Hello");  // 関数をエクスポート
console.log("this is module.ts");  // 確認用の出力
index.ts(インポートする側のモジュール)
import { hello } from "./module.js"; // hello をインポート
hello(); // hello を実行
console.log("this is index.ts");  // 確認用の出力

コンパイルして実行すると以下のようにインポートされる側のモジュールの console.log() が先に実行されます。

% npx tsc // コンパイル
% node dist/index.js // 実行
this is module.ts
Hello
this is index.ts

以下のように export が記述されていないファイル(スクリプト)でも、そのスクリプトが実行されます。

foo.ts
const foo = "Foo";
console.log(foo);
index.ts
import "./foo.js" // Foo が出力される
エイリアス

名前つきエクスポート/インポートの import 文や export 文の {} の中で、as を使って別名(エイリアス)、つまり異なる変数名を指定することで、宣言済みの変数を違う名前で名前つきエクスポートしたり、インポートした変数を異なる名前で利用できます。

異なる変数名(エイリアス)でエクスポート

以下のように as の後にエクスポートしたい変数名(エイリアス)を指定します。

module.ts
const name = "foo";
const age = 22;
// name を fooName という名前でエクスポート
export { name as fooName, age };

インポートする側では、エイリアス(別名)を指定します。

index.ts
import { fooName, age } from "./module.js";
console.log(fooName, age);

インポートした変数を異なる名前で利用

インポートする側でも、as を使って変数名を変えることができます。

以下の場合、age という名前でエクスポートされた変数を index.ts の中では fooAge という変数(エイリアス)で使うことができます。

index.ts
import { fooName, age as fooAge } from "./module.js";
// age という名前でエクスポートされた変数を fooAge として使う
console.log(fooName, fooAge);

デフォルトエクスポート

デフォルトエクスポートはモジュールごとに1つしかエクスポートできない特殊なエクスポートで、モジュールがデフォルトの機能を持つことができるように設計されたものです。

以下がデフォルトエクスポートの構文です。式の評価結果をそのモジュールのデフォルト値としてエクスポートします。export default は1つのモジュールに1つしか記述できません。

export default 構文
export default 式;

以下は "foo" という値("foo" という式の評価値)をデフォルトエクスポートする例です。

// "foo" という値をデフォルトエクスポート
export default "foo";

以下は変数 foo の値をデフォルトエクスポートする例です。

const foo = "foo";
// 変数の値(式の評価結果)をデフォルトエクスポートする
export default foo;

export default 構文ではあらゆる式が許可されます。

export default 1 + 2; // 式の評価結果 3 がデフォルトエクスポートされえる

以下は変数 hello の評価結果(関数)をデフォルトエクスポートする例です。

const hello = () => {
  console.log("Hello!");
}
// 変数 hello の評価結果(関数)をデフォルトエクスポートする
export default hello;

export default 構文に関数やクラスを直接指定すると、式ではなく宣言としてエクスポートされます。

export default function hello() {
  console.log("Hello!");
}

また、関数やクラスの宣言は無名にすることができます(宣言と同時にデフォルトエクスポートする場合、関数やクラスの名前を省略することができます)。

export default function() {
  console.log("Hello!");
}
上記をアロー関数で記述した場合
export default () => {
  console.log("Hello!");
}

変数の場合は、宣言とデフォルトエクスポートを同時に行うことはできません。以下はエラーになります。

export default const foo = "foo";  // エラー(Expression expected)
export default const hello = () => {  // エラー(Expression expected)
  console.log(`Hello, my name is ${name}.`)
}
デフォルトインポート

デフォルトエクスポートされたものは、以下のようなデフォルトインポートの構文を使ってインポートすることができます。※ どのような変数名にするかは、インポートする側が決めることができます。

デフォルトインポートの構文
import 変数名 from "モジュールのパス";

例えば、以下のようなモジュール module.ts でデフォルトエクスポートされている場合、

module.ts
const name = "foo";
// デフォルトエクスポート
export default name;

同じ階層にあるモジュール index.ts では以下のようにインポートすることができます。

index.ts
// デフォルトインポート
import fooName from "./module.js";
console.log(fooName); // foo
export default を使わない

実際にはデフォルトエクスポートは、default という固有の別名(エイリアス)の名前つきエクスポートと同じものです。そのため、前述の module.ts は以下のように記述しても同じことになります。

module.ts
const name = "foo";
// デフォルトエクスポート
export { name as default };

名前つきインポートにおいては default という名前でデフォルトインポートすることができます。

以下のように、名前つきインポートで default という名前でエクスポートされたものを、as を使って別の変数名として使用することができます。default は予約語なので、必ず as を使ってエイリアス(別名)をつける必要があります。

index.ts
// デフォルトインポート
import { default as fooName } from "./module.js";
console.log(fooName); // foo
名前付きエクスポート/インポートと併用

デフォルトエクスポート/インポートは、名前付きエクスポート/インポートと併用することができます。

以下は関数 hello をデフォルトエクスポートし、変数 name と age を名前付きエクスポートする例です。

module.ts
// 名前付きエクスポート
export const name = "foo";
export const age = 22;

// デフォルトエクスポート
export default function hello() {
  console.log(`Hello, my name is ${name}.`)
}

以下は上記でデフォルトエクスポートされた関数と名前付きエクスポートされた変数を1つの import 宣言にまとめてインポートする例です。

この時、まずデフォルトインポートする変数を書き、カンマで区切って {} の中に名前付きインポートの変数名のリストを記述します。

index.ts
// デフォルトインポートと名前付きインポートをまとめて記述
import helloFunc, { name, age } from "./module.js";
helloFunc(); // Hello, my name is foo.
console.log( name, age); // foo 22

すべてをインポート

import * as 構文を使って、すべての名前つき及びデフォルトエクスポートをまとめてオブジェクト(名前空間オブジェクト)としてインポートすることができます。以下の構文の「変数名」で指定された変数にオブジェクトが代入されます。

名前空間オブジェクト(Namespace Object)はインポートするモジュールのすべてのエクスポートを格納したオブジェクトになります。

import * as 変数名 from "モジュールのパス";

この方法では、モジュールの全てのエクスポートを名前空間オブジェクトとして取得して、それらをオブジェクトのメンバー(プロパティ)として利用します。

以下のようなモジュール module.ts がある場合、

module.ts
export const name = "foo";
export const age = 22;
export const hello = () => console.log("Hello");
export function greeting() {
console.log(`My name is ${name} and ${age} years old`);
}
export default { foo: "foo"};  // デフォルトエクスポート

以下のようにモジュール module.ts のすべてのエクスポートをオブジェクト(以下の場合は myModule)として取得して使用できます。

また、default という特別なプロパティ名を使ってデフォルトエクスポートにもアクセスできます。

index.ts
import * as myModule from "./module.js";

console.log(myModule.name, myModule.age); // foo 22
myModule.hello(); // Hello
myModule.greeting(); // My name is foo and 22 years old

// default というプロパティ名を使ってデフォルトエクスポートにもアクセスできる
console.log(myModule.default);  // {foo: 'foo'}

この方法の場合、オブジェクトのメンバーとして利用できるようにすることで独自の名前空間(Namespace)を持たせる効果があり、変数名の名前の衝突を回避することができます。

再エクスポート

再エクスポートは、別のモジュールからインポートしたものを自分自身からエクスポートし直すことで、複数のモジュールからエクスポートされたものをまとめたモジュールを作る(1つのモジュールに集約する)場合などに使われます。

再エクスポートの構文にはいくつかのパターンがありますが、基本的には export 文のあとに from を続けて、別のモジュール名を指定します。

以下は指定したモジュールからエクスポートされているすべての変数が再エクスポートされます。但し、再エクスポートの場合、デフォルトエクスポートは含まれません。

export * from "モジュールのパス";

以下は指定したモジュールからすべてのエクスポート(デフォルトエクスポートを含む)を変数名に指定したオブジェクトとして再エクスポートします。

export * as 変数名 from "モジュールのパス";

以下は指定したモジュールからエクスポートされている変数名を指定して再エクスポートします。変数名のリストでは as を使って別名で再エクスポートすることができます。

export { 変数名のリスト } from "モジュールのパス";

最初の構文では、デフォルトエクスポートは含まれないので、以下のように明示的に別途デフォルトエクスポートを再エクスポートすることができます。

export * from './module.js'; // すべての名前付きエクスポートを再エクスポート
export { default } from './module.js'; // デフォルトエクスポートを再エクスポート

dynamic import: import()

dynamic import は ES2020 で導入された動的に import を非同期的に実行するための機能です。

import(モジュール名) 

import() は普通の import 宣言と異なり、トップレベルでなくても使用でき、モジュール以外の環境(スクリプト)でもモジュールをインポートすることができます。

import() にモジュールのパスを渡すと、そのモジュールの名前空間オブジェクト(モジュールのすべてのエクスポートを格納したオブジェクト)を結果として持つ Promise を返します。

例えば以下のようなモジュールをインポートする場合、

module.ts
export const name = "foo";
export const age = 22;
export const hello = () => console.log("Hello");
export function greeting() {
console.log(`My name is ${name} and ${age} years old`);
}
export default { foo: "foo"};  // デフォルトエクスポート

import() は Promise を返すので、Promise の then メソッドを使えば、コールバック関数の引数に名前空間オブジェクトが渡されます。

モジュールのパスは普通の import 宣言同様、カレントディレクトリを表す ./ や1つ上のディレクトリを表す ../ を使って指定し、ファイル名は、コンパイル後の拡張子.jsを指定します。

import() での読み込みは非同期処理として扱われるので、以下を実行すると先に「同期的処理」が出力され、その後「foo 22」などが(非同期的なタイミングで)出力されます。

index.ts
import('./module.js').then((response) => {
  console.log(response.name, response.age); // foo 22
  console.log(response.default); // {foo: 'foo'}
  response.hello(); // Hello
  response.greeting(); // My name is foo and 22 years old
});

console.log('同期的処理');

import() はトップレベルでなくても使用できるので、条件文のブロックや関数の中で使用することもできます。また、import() に指定するモジュールのパスは変数で指定することもできます。

以下では then メソッドのコールバック関数の引数に渡されるオブジェクトを分割代入を使って選択的に変数に代入しています。

index.ts
const url = './module.js';
const flag = true;
if (flag) {
  // トップレベルでなくても使用できる
  import(url).then(({ name, age }) => {
    console.log(name, age); // foo 22
  });
}

型のエクスポートとインポート

TypeScript では型のエクスポートとインポートが可能です。

エクスポート

type 文(型エイリアス)や interface(インターフェース)は、通常の JavaScript 宣言と同様、export を使用してエクスポートすることができます。

export type 型名 = /* 型 */;
export interface 型名 { /*  型 */ };

以下は type 文を使った Person という型と const 宣言したオブジェクトをエクスポートする例です。

module.ts
// 型のエクスポート
export type Person = {
  name: string;
  age: number;
}

// オブジェクトのエクスポート
export const foo: Person = {
  name: "foo",
  age: 22
}

また、通常の名前付きエクスポート同様、宣言とは別に定義済みの型を export { 変数リスト } の構文でエクスポートすることもできます。この場合、変数と型を一緒にエクスポートすることができます。

以下は上記と同じことです。

module.ts
type Person = {
  name: string;
  age: number;
}

const foo: Person = {
  name: "foo",
  age: 22
}

export { Person, foo };

インポート

上記の方法でエクスポートされた型は、変数と同様に import を使ってインポートすることができます。

index.ts
// 型と変数をインポート
import { Person, foo } from "./module.js";

// インポートした型で型注釈
const bar: Person = {
  name: "bar",
  age: 18
}

// インポートした変数をスプレッド構文で展開
const person :Person = {...foo}

console.log(foo, bar); // {name: 'foo', age: 22} {name: 'bar', age: 18}

import * as構文を使ってインポートすることもできます(すべてをインポート)。

通常の変数同様、インポートした型も名前空間オブジェクトのプロパティとして参照できます。

index.ts
import * as mod from "./module.js";

// 名前空間オブジェクト mod のプロパティとして参照
const bar: mod.Person = {
  name: "bar",
  age: 18
}

const person :mod.Person = {...mod.foo}

console.log(person, bar); // {name: 'foo', age: 22} {name: 'bar', age: 18}

Type-only imports and exports

Type-only imports and exports は、モジュールから型情報のみをインポート・エクスポートするための機能(構文)です。

多くの場合、この機能を使わなくても、前述の通常の importexport を使って、型や変数をインポート・エクスポートすることができます。但し、TypeScript コンパイラ以外の方法を使って TypeScript を JavaScript にコンパイルする場合にこの機能が必要になる場合があります。

export type { }

export type {}という構文を使うと、エクスポートされたものは型としてのみ使用できます。

以下はこの構文を使って型情報をエクスポートする例です。

この構文では、型とともに通常の変数をエクスポートすることは可能ですが、インポートした側ではその変数を値として使用することはできません(型としてのみ使用可能)。

module.ts
type Person = {
  name: string;
  age: number;
}

const foo: Person = {
  name: "foo",
  age: 22
}

// 型情報のみをエクスポート(変数も一緒にエクスポートすることは可能)
export type { Person, foo };

インポートする側では、export type {}でエクスポートされた型を変数と一緒に import でインポートすることができます。

但し、以下のようにインポートした型情報は型として使用することはできますが、インポートした変数は値として使用するとコンパイルエラー及び実行時エラーになります。

index.ts
import { Person, foo } from "./module.js";

const person1: Person = {
  name: "bar",
  age: 18
}

const person2 = foo;  // エラー
//'foo' cannot be used as a value because it was exported using 'export type'.
// Uncaught ReferenceError: foo is not defined(実行時エラー)

この構文を使った場合、コンパイル後の JavaScript ではエクスポートは export {} となり、何もエクスポートされなくなります。そのため、インポート側で変数にアクセスするとコンパイルエラーと共に実行時エラー(Uncaught ReferenceError)になります。

module.js(コンパイル後の JavaScript)
const foo = {
  name: "foo",
  age: 22
};
export {};  // 変数 foo もエクスポートされない

このようにexport type {}構文で変数をエクスポートした場合、インポート側で値として使用することはできませんが、型のコンテキストで使用することはできます。

例えば、以下のようにインポートした変数 foo を型のコンテキストで typeof と使用することはできます。

index.ts
import { Person, foo } from "./module.js";

const person1: Person = {
  name: "bar",
  age: 18
}

// 型のコンテキストで typeof と使用
type Foo = typeof foo; // OK

import type

import typeは型情報だけをインポートする構文です。

module.ts(通常のエクスポート)
type Person = {
  name: string;
  age: number;
}

const foo: Person = {
  name: "foo",
  age: 22
}

//通常のエクスポート
export { Person, foo };

import typeでインポートしたものは、型としてのみ使用できます。

index.ts
// 型情報だけをインポート
import type { Person, foo } from "./module.js";

const person1: Person = {
  name: "bar",
  age: 18
}

// 型のコンテキストで typeof と使用
type Foo = typeof foo; // OK

// 値として使用するとエラーになります
const person2 = foo; // エラー
// 'foo' cannot be used as a value because it was imported using 'import type'.
// Uncaught ReferenceError: foo is not defined(実行時エラー)

type を接頭辞として付けられたインポートとエクスポート

import type 構文を使う場合、モジュールから通常のインポートと型情報のみのインポートをするには、import宣言を別々に記述する必要があります。

例えば、前述の例の場合、以下のように記述すれば、変数 foo を値として使用することができます。

index.ts
// 型情報のみのインポート
import type { Person } from "./module.js";
// 通常のインポート
import { foo } from "./module.js";

const person1: Person = {
  name: "bar",
  age: 18
}

type Foo = typeof foo; // OK
const person2 = foo; // OK(値として使用)

上記は以下のように型情報のみをインポートする変数名の前に type を接頭辞を付けることで、1つのimport宣言にまとめることができます。

// 型情報のみをインポートする変数名の前に type を付ける
import  { type Person, foo } from "./module.js";

const person1: Person = {
  name: "bar",
  age: 18
}

type Foo = typeof foo; // OK
const person2 = foo; // OK

スクリプトとモジュール

JavaScrript(TypeScript)のファイルはスクリプトとモジュールに分類することができます。

TypeScript の場合は、コード中に import や export が使われている場合は自動的にモジュールとして扱われ、そうでない場合はスクリプトとして扱われます。

TypeScript 4.7 以降では、tsconfig.json の compilerOptions の module や moduleDetection の設定によってもスクリプトとモジュールの判定方法が変わります。

モジュールのスコープ

ファイルがモジュールとして扱われる場合、以下のような特徴があります。

  • 各モジュールには独自のトップレベルのスコープがあります
  • トップレベルでの変数はグローバルではなくモジュール内のローカル変数として定義されます
  • トップレベルの this は window ではなく undefined になります
  • モジュール内で定義された変数を変更できるのはその変数を宣言したモジュール内のみになります

モジュール内で定義された変数はそのモジュール内をスコープとして持ち、エクスポートされない限り、モジュール内のローカル変数となり、他のモジュールから参照することはできません。

明示的に export をつけた値(エクスポートした値)だけが公開され、他のモジュールから参照できます。

例えば、以下のモジュール counter.ts は関数 countUp のみをエクスポートしています。

counter.ts
let count = 0;
// 以下の関数をエクスポート
export const countUp = () => {
  return  ++ count;
}

上記のモジュール counter.ts を使う側ではエクスポートされている countUp のみをインポートすることができます。変数 count をインポートしようとするとコンパイルエラーになります。

index.ts
import { countUp } from "./counter.js";

console.log(`count is ${countUp()}`);  // count is 0
console.log(`count is ${countUp()}`);  // count is 1
console.log(`count is ${countUp()}`);  // count is 2

モジュール counter.ts を以下のように書き換えて変数 count もエクスポートするようにすれば、

counter.ts
// count もエクスポート
export let count = 0;

export const countUp = () => {
  return  ++ count;
}

使う側でも変数 count の値を参照することができます。

但し、インポートした変数を変更しようとすると、コンパイルエラーになり、変更することはできません。

index.ts
import { countUp, count } from "./counter.js";

countUp();
console.log(`count is ${count}`);  // count is 1

// 外部からモジュールの変数を変更できないので以下はエラーになる
count = 10;
// Cannot assign to 'count' because it is an import.

スクリプトのスコープ

TypeScript においてファイルがスクリプトとして扱われる場合、トップレベルに定義した変数や型のスコープはファイル内だけではなくプロジェクト全体になります。

そのため、ファイルがスクリプトとして扱われる場合、プロジェクト内で別ファイルなのに同じ名前の変数を定義すると「同じ名前の変数を再宣言することはできない」というコンパイルエラーが発生します。

例えば、以下のような同じプロジェクト内にモジュールでない2つのファイルがある場合、同じ名前の変数があるとコンパイルエラーになります。

foo.ts
const myName = "foo";  // コンパイルエラー
// Cannot redeclare block-scoped variable 'myName'. 
bar.ts
const myName = "bar";  // コンパイルエラー
// Cannot redeclare block-scoped variable 'myName'.

コンパイルエラーにならないようにするには以下のような方法があります。

  1. 変数名を異なる名前に変更する
  2. export {}; を追加して、ファイルをモジュールとして扱う
  3. ブロック内に記述する
  4. tsconfig.json の設定を変更する

簡単なのは 2. のexport {}; を追加して、ファイルをモジュールとして扱うようにする方法です。

例えば、上記の例の場合、foo.ts または bar.ts のいずれか(または両方)にexport {};を追加すればエラーは消えます。export {};は空の変数(0個の変数)をエクスポートするという意味で、何もエクスポートしませんが、export を使っているのでファイルはモジュールとして扱われます。

foo.ts
const myName = "foo";
export {}; // 空の変数をエクスポートしてこのファイルをモジュールにする

また、name などの特定の識別子を使って変数を定義すると、同様のエラーが発生します。

これは TypeScript をインストールする際に同梱される DOM API の型定義ファイル(lib.dom.d.ts)に name などの変数がすでに定義されているために発生します。

このような場合も上記のいずれかの方法でエラーを回避することができます。

関連ページ:TypeScript の変数定義時のコンパイルエラー

今まで使えていた型がコンパイルエラーになる

例えば、以下のようなスクリプトとして扱われる2つの TypeScript のファイルがある場合、

person.ts
type Person = {
  name: string;
  age: number;
}
index.ts
const foo: Person = {
  name: "foo",
  age: 22
}

上記の index.ts では foo の型注釈に person.ts で定義されている Person 型を使用していますが、問題なくコンパイルできてしまいます。

これも、スクリプトとして扱われる person.ts の中で定義されている Person 型のスコープがプロジェクト全体になっているためです。

もし、person.ts を以下のように Person をエクスポートするように書き換えてコンパイルすると、

person.ts
// Person をエクスポート(ファイルはモジュールになる)
export type Person = {
  name: string;
  age: number;
}

index.ts では Cannot find name 'Person' というコンパイルエラーになります。

これは person.ts がモジュールになったため、Person のスコープが person.ts の中だけになり、index.ts から Person を参照できなくなってしまったためです。

index.ts でのエラーを解消するには、明示的に Person 型をインポートします。この場合、型情報のみをインポートするので、Type-only を使って import type { Person } from "./person.js";import { type Person } from "./person.js"; でも同じです。

// Person 型をインポート
import { Person } from "./person.js";

const foo: Person = {
  name: "foo",
  age: 22
}

このように、今まで使えていた型が急にコンパイルエラーで使えなくなった場合、元の型がスクリプトからモジュールに変わった可能性があります。

TypeScript でのスコープ

この項の最初に記載したように、ファイルがスクリプトとして扱われる場合にトップレベルに定義した変数や型のスコープがプロジェクト全体になるのは、TypeScript においての場合です。

例えば、以下の2つのファイルをコンパイルしてもコンパイルエラーが発生しません。

但し、コンパイル後の index.js では実行時エラー(Uncaught ReferenceError)が発生します。

foo.ts
const foo = "Foo";
index.ts
console.log(foo);  //コンパイルエラーは発生しないが、実行時エラーになる
// Uncaught ReferenceError: foo is not defined

この場合、foo.ts での foo の定義では const 宣言されているので、foo の型は文字列リテラル型 "Foo" になるので、以下のように使うことはできます。

index.ts
const bar: typeof foo = "Foo";

但し、何らかの理由で後から foo.ts をモジュールに変更すると、index.ts では Cannot find name 'foo' というコンパイルエラーになるので、このような使い方はしないほうが良いかと思います。