※ こちらの「コード品質向上の重要性:読みやすく、使いやすいシステムを作るために」という記事を読んでから、本記事を読んでいただけると幸いです。
また、「コード品質向上の重要性:読みやすく、使いやすいシステムを作るために」の補足記事として、以下の記事も作成しました。こちらも併せて読んでいただけると幸いです。
はじめに
モジュールは、JavaScript(およびTypeScript)アプリケーション開発において不可欠な要素です。適切にモジュールを設計し、必要なものだけを共有することで、コードの可読性、保守性、再利用性が向上します。
この記事では、TypeScript環境における import と export の基本的な仕組みから、より効率的なモジュール設計のためのテクニックまでを解説します。コード例を交えながら、あなたのプロジェクトで活かせる実践的な知識が提供できれば幸いです。
基本編
この章で想定するフォルダ構造
この「基本編」では、以下のシンプルなフォルダ構造を想定しています。
src/
├── services/
│ ├── calculator.ts
│ └── user-service.ts
├── types/
│ ├── user.ts
│ └── greeter.ts
├── utils/
│ ├── math.ts
│ └── string.ts
└── app.ts
export の基本: モジュールから何かを公開する
モジュールから値や関数、型などを外部に公開するには export キーワードを使います。
名前付きエクスポート (Named Exports)
複数の値をエクスポートする際に便利です。インポートする側は、エクスポートされた元の名前を使って値を受け取ります。
export const add = (a: number, b: number): number => a + b;
export const subtract = (a: number, b: number): number => a - b;
デフォルトエクスポート (Default Exports)
モジュールから単一の主要な値をエクスポートする際に使用します。1つのモジュールにつき1つしかデフォルトエクスポートはできません。インポートする側は、任意の名前を付けて値を受け取れます。
const multiply = (a: number, b: number): number => a * b;
export default multiply;
デフォルトエクスポートのメリットとデメリット
-
メリット:
-
簡潔なインポート構文:
import MyModule from './my-module';のように波括弧が不要で、シンプルに記述できます。 - 「そのモジュールの主要な機能」を明確化: モジュールが提供するメインの機能やクラス、関数がどれであるかを明示的に示せます。
-
簡潔なインポート構文:
-
デメリット:
- 名前の衝突リスク: インポート時に任意の名前を付けられるため、誤って既存の名前と衝突したり、チーム内で一貫性のない名前が使われたりする可能性があります。
- 1モジュール1デフォルト: 複数の値をデフォルトエクスポートすることはできません。
エクスポートと型定義
カスタムの型(インターフェースや型エイリアスなど)や、クラスもexportすることで他のモジュールと共有できます。
export interface User {
id: number;
name: string;
}
export class Greeter {
constructor(public greeting: string) {}
greet(): string {
return `Hello, ${this.greeting}!`;
}
}
import の基本: 他のモジュールから何かを取り込む
他のモジュールでエクスポートされた値や型を利用するには import キーワードを使います。
名前付きインポート (Named Imports)
名前付きエクスポートされた特定の値を、波括弧 {} を使ってインポートします。必要に応じてasキーワードで別名を付けることも可能です。
import { add, subtract } from './utils/math';
// 別名でインポートする例
// import { add as plus } from './utils/math';
console.log(add(5, 3));
デフォルトインポート (Default Imports)
デフォルトエクスポートされた値をインポートします。インポート時には波括弧は不要で、任意の名前を付けて受け取ります。
import calculateProduct from './services/calculator';
console.log(calculateProduct(5, 3));
全てをインポート (Namespace Imports)
モジュール内の全てのエクスポートを、一つのオブジェクトとしてまとめてインポートします。エクスポートされた各値には、このオブジェクトのプロパティとしてアクセスします。
import * as MathUtils from './utils/math';
console.log(MathUtils.add(5, 3));
console.log(MathUtils.subtract(10, 4));
型のみのインポート (Type-Only Imports)
TypeScript 3.8以降で利用できる import type を使うと、ランタイム時には含まれず、型チェックのためだけにインポートを行うことができます。
import type { User } from '../types/user'; // Userは型情報のみをインポート
const getUserName = (user: User): string => user.name;
import type の具体的な活用シーン
import type は、特に以下のような場面で有効です。
-
循環参照の回避: 相互に型を参照し合うモジュールがある場合、通常の
importだと循環参照となりコンパイルエラーやランタイムエラーの原因になることがあります。import typeを使うと、ランタイムコードには含まれないため、この循環参照を安全に解決できます。 -
バンドルサイズの最適化: ビルドツール(Webpack, Rollupなど)がツリーシェイキングを行う際、通常の import だと型情報しか使われていなくても、そのモジュール全体をバンドルに含めてしまうことがあります。
import typeを明示することで、型のみの依存であることが明確になり、不要なJavaScriptコードがバンドルされるのを防ぐことができます。 - 意図の明確化: 「これは型のためだけにインポートしている」という開発者の意図をコード上で明確に示せます。
必要なモノだけ共有させるための実践的テクニック
1. プライベートな実装の保持
モジュール内部で利用する定数や変数、関数、クラスなどを、export キーワードを付けずに宣言することで、外部からアクセスできないプライベートな実装として隠蔽できます。これにより、カプセル化を強化し、モジュールの利用者は公開されたAPIだけを意識すればよくなり、内部変更の影響を受けにくくなります。
フォルダ構造の例:
src/
├── utils/
│ └── internal-helper.ts
└── app.ts
以下コードの具体例です。
// この関数はモジュール内部でのみ使用されるプライベートなヘルパー関数です
const _internalCalculation = (value: number): number => {
return value * 2 + 10;
};
export const processValue = (input: number): number => {
const result = _internalCalculation(input);
return result;
};
import * as Helper from './utils/internal-helper';
console.log(Helper.processValue(5)); // 20 (5 * 2 + 10)
// Helper._internalCalculation はエクスポートされていないため、アクセスできません。
// console.log(Helper._internalCalculation(3));
// ↑ エラー: Property '_internalCalculation' does not exist...
2. 再エクスポート (Re-exporting) の適切な利用
複数のモジュールを一つのファイルに集約してエクスポートし、インポートを簡素化させることができます。特にフォルダ構造が深くなった場合に有効です。
今回は、より大きなアプリケーションを想定し、全てのモジュールを src/modules フォルダ以下に集約してみます。
以下、フォルダ構成です。
src/
├── modules/
│ ├── services/
│ │ ├── auth.ts
│ │ ├── calculator.ts
│ │ └── reporting.ts
│ ├── types/
│ │ ├── greeter.ts
│ │ ├── product.ts
│ │ └── user.ts
│ ├── utils/
│ │ ├── math.ts
│ │ └── string.ts
│ └── index.ts
└── app.ts
各モジュールファイルの内容:
読み飛ばしてもあまり問題ないです。
export const login = (username: string, password: string): boolean => {
// ... 認証ロジック
return true;
};
export const logout = (): void => {
// ... ログアウトロジック
};
const multiply = (a: number, b: number): number => a * b;
export default multiply;
// 『2.2. プライベートな実装との組み合わせ』でのみ使います
export const generateSummaryReport = (data: number[]): string => {
const total = data.reduce((sum, current) => sum + current, 0);
const average = total / data.length;
return `簡易レポート: 平均値 ${average.toFixed(2)}`;
};
export class Greeter {
constructor(public greeting: string) {}
greet(): string {
return `Hello, ${this.greeting}!`;
}
}
export interface Product {
id: number;
name: string;
price: number;
}
export interface User {
id: number;
name: string;
}
export const add = (a: number, b: number): number => a + b;
export const subtract = (a: number, b: number): number => a - b;
export const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1);
export const reverse = (str: string): string => str.split('').reverse().join('');
2.1. 基本編
このサブセクションでは、すべての公開されたモジュールをindex.ts 経由で再エクスポートする基本的な方法を示します。
再エクスポートの例 (src/modules/index.ts の作成):
src/modules ディレクトリの直下に index.ts を作成し、各サブフォルダから必要なモノを再エクスポートします。
// services フォルダから特定のものを再エクスポート
export { login, logout } from './services/auth';
export { default as calculateProduct } from './services/calculator';
export { generateSummaryReport } from './services/reporting';
// types フォルダから型を再エクスポート
export type { Product } from './types/product';
export type { User } from './types/user';
export { Greeter } from './types/greeter';
// utils フォルダから全てを再エクスポート
export * from './utils/math';
export * from './utils/string';
なぜindex.tsと命名するのか
src/modules/index.ts が存在する場合、import * as AliasName from './modules'; のようにフォルダ名だけでインポートし、全てを名前空間として利用できます。したがって、この index.ts が、src/modules 以下のモジュールへの統一された入り口となります。
import * as AllModules from './modules'; // 名前空間インポート
/*
以下のように名前付きインポートを用いても問題ありません。
import {
add,
capitalize,
calculateProduct,
login,
User,
Product,
Greeter,
generateSummaryReport
} from './modules';
*/
console.log(AllModules.add(10, 5));
console.log(AllModules.capitalize('hello'));
console.log(AllModules.calculateProduct(4, 6));
console.log(AllModules.login('user', 'pass'));
const user: AllModules.User = { id: 1, name: 'Alice' };
const product: AllModules.Product = { id: 101, name: 'Laptop', price: 1200 };
const greeter = new AllModules.Greeter('World');
console.log(user.name);
console.log(product.price);
console.log(greeter.greet());
const dataForReport = [100, 200, 300];
console.log(AllModules.generateSummaryReport(dataForReport));
2.2. プライベートな実装との組み合わせ
ここでは、サブモジュールでエクスポートされたモノのうち一部を最終的なエクスポートに含めない方法について紹介します。
具体的には、reporting.ts は内部で utils/math.ts の add 関数を利用するために utils/math.ts で add 関数を export しますが、src/modules を import しても utils/math.ts の add 関数は import できない、という仕組みの実装方法を紹介します。
src/modules/services/reporting.ts の更新:
import { add } from '../utils/math'; // サブモジュール間の連携
const _calculateAverage = (data: number[]): number => {
if (data.length === 0) return 0;
const total = data.reduce((sum, current) => add(sum, current), 0); // utils/math.ts の add を使用
return total / data.length;
};
export const generateSummaryReport = (salesData: number[]): string => {
const average = _calculateAverage(salesData);
return `今日の平均売上: ${average.toFixed(2)}円`;
};
src/modules/index.ts の更新:
generateSummaryReport は再エクスポートしますが、utils/math.ts の add 関数は、index.ts から直接再エクスポートしません。これにより、src/modules を経由したインポートでは add 関数は見えませんが、reporting.ts の内部では利用可能です。
export { login, logout } from './services/auth';
export { default as calculateProduct } from './services/calculator';
export { generateSummaryReport } from './services/reporting';
export type { Product } from './types/product';
export type { User } from './types/user';
export { Greeter } from './types/greeter';
export { capitalize, reverse } from './utils/string';
// utils フォルダから必要なものを個別に名前付きエクスポートします。
// ここでは utils/math.ts の add は再エクスポートしません。
// しかし、reporting.ts が内部で利用することは可能です。
export { subtract } from './utils/math';
app.ts からの利用例
src/app.ts からは、src/modules を通して generateSummaryReport を利用できますが、add 関数は AllModules 名前空間には含まれません。
import * as AllModules from './modules';
const dailySales = [1000, 1500, 1200, 2000, 800];
console.log(AllModules.generateSummaryReport(dailySales));
// AllModules には capitalize, reverse, subtract が含まれますが、add は含まれません。
console.log(AllModules.capitalize('world'));
console.log(AllModules.subtract(10, 3));
// console.log(AllModules.add(1,2)); // エラー: Property 'add' does not exist on type 'typeof import("./modules")'.
// これは src/modules/index.ts で add を再エクスポートしていないためです。
// どうしても add をインポートしたい場合は、'src/modules/utils/math.ts'を直接指定します。
// ただし、 add をインポートせずに済むようにモジュール側の仕組みを整えた方が良いでしょう。
import { add } from './modules/utils/math';
console.log(add(5, 5));
このように、モジュールは内部で他のモジュールを利用しつつ、その実装詳細を隠蔽し、再エクスポートによって必要な機能だけを公開することができます。これにより、モジュール間の依存関係を整理し、コードの凝集度を高めることが可能です。
3. パスエイリアス (Path Aliases) の利用
長い相対パスを避け、絶対パスのような形でインポートを簡潔にするための方法として、TypeScriptのパスエイリアス機能があります。これは tsconfig.json で設定します。
tsconfig.json の設定例:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@modules/*": ["src/modules/*"]
}
}
}
パスエイリアスを利用したインポートの例:
先程のフォルダ構造から app.tsの場所を移動させます。
具体的には以下のフォルダ構成になります。
/(root)
├──src/
│ ├── modules/
│ │ ├── services/
│ │ │ ├── auth.ts
│ │ │ ├── calculator.ts
│ │ │ └── reporting.ts
│ │ ├── types/
│ │ │ ├── greeter.ts
│ │ │ ├── product.ts
│ │ │ └── user.ts
│ │ ├── utils/
│ │ │ ├── math.ts
│ │ │ └── string.ts
│ │ └── index.ts
│ └── some/
│ └── folder/
│ └── app.ts
└── tsconfig.json
import * as AllModules from '@modules'; // 本来は'../../modules'を指定すべきですが、@modulesでOK
console.log(AllModules.capitalize('hello'));
AllModules.login('admin', 'password');
const user: AllModules.User = { id: 2, name: 'Bob' };
console.log(user.name);
パスエイリアスは、特に深いネストになったフォルダ構造を持つ大規模プロジェクトで、インポート文の可読性を大幅に向上させます。
まとめ
この記事では、TypeScriptにおける import と export の基本から、プライベートな実装の保持、再エクスポート によるモジュールの集約、index.ts を活用したインポートの簡素化、そして パスエイリアス によるインポートパスの最適化について解説しました。
これらの知識を活かすことで、よりクリーンで保守性の高いアプリケーションを開発できるようになります。モジュールの設計は、アプリケーションの成長と共にその重要性が増していきます。この記事で学んだことを参考に、あなたのプロジェクトで最適なモジュール戦略を構築してください。