TypeScriptでサービス・モデル・ユーティリティを分離する設計例を初心者向けに解説
生徒
「TypeScriptでプログラムを書いていると、どんどんコードが増えてきて、どこに何があるかわからなくなってしまいます…」
先生
「それは多くの初心者が経験する問題ですね。TypeScriptでは、サービス・モデル・ユーティリティという役割ごとにファイルを分離する設計が推奨されています。」
生徒
「サービス、モデル、ユーティリティって何ですか?どうやって分けるんですか?」
先生
「それでは、それぞれの役割と、実際にどのように分離して設計するかを詳しく見ていきましょう!」
1. サービス・モデル・ユーティリティとは何か
TypeScriptでアプリケーションを開発する際、コードを役割ごとに分離することが非常に重要です。これは「関心の分離」と呼ばれる設計の基本原則で、コードの見通しをよくし、メンテナンスしやすくするための手法です。
モデル(Model)
モデルとは、アプリケーション内で扱うデータの形を定義するものです。例えば、ユーザー情報や商品情報など、プログラムで使うデータの「型」や「構造」を決めます。現実世界で例えるなら、設計図のようなものです。家を建てる前に設計図が必要なように、データを扱う前にその形を決めておくのです。
サービス(Service)
サービスとは、アプリケーションのビジネスロジックを担当する部分です。ビジネスロジックとは「何をするか」という処理の内容のことで、例えばユーザーを登録する、商品を検索する、データを計算するといった具体的な機能を実装します。料理に例えると、レシピに従って実際に調理する工程がサービスに当たります。
ユーティリティ(Utility)
ユーティリティとは、アプリケーション全体で共通して使える便利な関数をまとめたものです。例えば、日付のフォーマット変換や文字列の加工など、どこでも使う可能性がある汎用的な機能を配置します。工具箱のようなもので、必要なときにいつでも取り出して使える道具を入れておく場所です。
2. モデルの設計例
まず、データの構造を定義するモデルから見ていきましょう。TypeScriptではinterfaceやtypeを使ってモデルを定義します。
ユーザーモデルの例
例えば、ユーザー情報を扱うアプリケーションを作る場合、ユーザーのデータ構造を定義します。ファイル名はuser.model.tsのように、わかりやすい名前を付けます。
// models/user.model.ts
export interface User {
id: number;
name: string;
email: string;
age: number;
isActive: boolean;
}
export interface UserCreateInput {
name: string;
email: string;
age: number;
}
ここではUserというインターフェースで、ユーザーが持つべき情報(ID、名前、メールアドレス、年齢、アクティブ状態)を定義しています。また、新しいユーザーを作成する際に必要な情報だけを持つUserCreateInputも定義しています。exportキーワードを使うことで、他のファイルからこのモデルを利用できるようにしています。
3. サービスの設計例
次に、実際の処理を行うサービスを作成します。サービスは、モデルで定義したデータを使って具体的な操作を実行します。
ユーザーサービスの例
ユーザーに関する処理をまとめたサービスを作成しましょう。ファイル名はuser.service.tsとします。
// services/user.service.ts
import { User, UserCreateInput } from '../models/user.model';
export class UserService {
private users: User[] = [];
private nextId: number = 1;
createUser(input: UserCreateInput): User {
const newUser: User = {
id: this.nextId++,
name: input.name,
email: input.email,
age: input.age,
isActive: true
};
this.users.push(newUser);
return newUser;
}
getUserById(id: number): User | undefined {
return this.users.find(user => user.id === id);
}
getAllUsers(): User[] {
return this.users;
}
updateUser(id: number, updates: Partial<User>): User | undefined {
const user = this.getUserById(id);
if (user) {
Object.assign(user, updates);
}
return user;
}
}
このサービスでは、ユーザーの作成、取得、更新といった操作をメソッドとして提供しています。importを使って、先ほど定義したモデルを読み込んでいます。これにより、型の安全性を保ちながら開発できます。Partial<User>は、Userの全てのプロパティをオプショナル(省略可能)にする型で、更新時に一部の情報だけを変更したい場合に便利です。
4. ユーティリティの設計例
アプリケーション全体で使える便利な関数をユーティリティとしてまとめます。
汎用ユーティリティの例
日付のフォーマット変換や文字列処理など、どこでも使える関数を配置します。ファイル名はformat.util.tsのようにします。
// utils/format.util.ts
export function formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
export function capitalizeFirstLetter(str: string): string {
if (str.length === 0) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
このユーティリティファイルでは、日付を「YYYY-MM-DD」形式にフォーマットする関数、文字列の最初の文字を大文字にする関数、メールアドレスが正しい形式かをチェックする関数を定義しています。これらは特定の機能に依存せず、アプリケーションのどこからでも呼び出せる汎用的な関数です。
5. 全てを組み合わせて使う実践例
それでは、これまでに作成したモデル、サービス、ユーティリティを組み合わせて、実際にアプリケーションを動かしてみましょう。
// app.ts
import { UserService } from './services/user.service';
import { formatDate, capitalizeFirstLetter, isValidEmail } from './utils/format.util';
const userService = new UserService();
// メールアドレスのバリデーション
const email = "test@example.com";
if (isValidEmail(email)) {
const newUser = userService.createUser({
name: capitalizeFirstLetter("taro"),
email: email,
age: 25
});
console.log("作成されたユーザー:", newUser);
console.log("作成日:", formatDate(new Date()));
// ユーザー情報の更新
const updated = userService.updateUser(newUser.id, {
age: 26
});
console.log("更新後のユーザー:", updated);
}
// 全ユーザーの取得
const allUsers = userService.getAllUsers();
console.log("全ユーザー:", allUsers);
実行結果は以下のようになります。
作成されたユーザー: { id: 1, name: 'Taro', email: 'test@example.com', age: 25, isActive: true }
作成日: 2026-01-14
更新後のユーザー: { id: 1, name: 'Taro', email: 'test@example.com', age: 26, isActive: true }
全ユーザー: [ { id: 1, name: 'Taro', email: 'test@example.com', age: 26, isActive: true } ]
6. ディレクトリ構造の推奨例
サービス・モデル・ユーティリティを分離する際は、ディレクトリ(フォルダ)構造も整理することが大切です。以下は推奨される構造の例です。
project/
├── src/
│ ├── models/
│ │ ├── user.model.ts
│ │ └── product.model.ts
│ ├── services/
│ │ ├── user.service.ts
│ │ └── product.service.ts
│ ├── utils/
│ │ ├── format.util.ts
│ │ └── validation.util.ts
│ └── app.ts
├── package.json
└── tsconfig.json
このように役割ごとにフォルダを分けることで、「どこに何があるか」が一目でわかり、新しい機能を追加する際もどこに配置すればよいかが明確になります。チームで開発する際にも、この構造があることで他の開発者が理解しやすくなります。
7. 分離設計のメリット
サービス・モデル・ユーティリティを分離する設計には、多くのメリットがあります。
コードの可読性向上
役割ごとにファイルが分かれているため、特定の機能を探すときにどこを見ればよいかがすぐにわかります。例えば、ユーザー関連の処理を修正したいときは、user.service.tsを開けばよいとすぐに判断できます。
再利用性の向上
ユーティリティとして定義した関数は、アプリケーションのどこからでも呼び出せるため、同じコードを何度も書く必要がありません。これにより開発効率が大幅に向上します。
テストのしやすさ
機能が分離されているため、それぞれを独立してテストすることができます。サービスだけをテストしたり、ユーティリティ関数だけをテストしたりすることが容易になります。
保守性の向上
バグ修正や機能追加の際、影響範囲が明確になります。例えば、日付フォーマットの変更が必要な場合、format.util.tsだけを修正すれば、それを使っている全ての箇所に変更が反映されます。
8. 初心者が陥りやすい注意点
この設計パターンを使う際に、初心者が注意すべきポイントをいくつか紹介します。
過度な分離は避ける
何でもかんでもファイルを分けすぎると、逆に管理が大変になります。小規模なプロジェクトでは、ある程度まとめておく方が効率的な場合もあります。プロジェクトの規模に応じて適切に判断しましょう。
循環参照に注意
ファイルAがファイルBをインポートし、ファイルBがファイルAをインポートするような循環参照は避けましょう。これはエラーの原因になります。依存関係は一方向に保つことが重要です。
命名規則の統一
ファイル名には一貫した命名規則を使いましょう。モデルは.model.ts、サービスは.service.ts、ユーティリティは.util.tsのように統一することで、チーム開発でも混乱が起きにくくなります。