Day 21:第3週総合練習「ユーザー管理システム」を型で設計する
第3週のラストは、ここまで学んできた TypeScriptの「型」 を総動員して、 小さな「ユーザー管理システム」を設計していく総合練習です。
今回のゴールはひとつです。
TypeScriptの「型」を使って、安全なプログラムを書けるようになること。
扱う型と機能は次のとおりです。
- 型
- User
- Admin
- Customer
- Guest
- Role
- Status
- Address
- 機能
- 登録
- 検索
- 削除
- 権限判定
- ステータス変更
ここから、ステップバイステップで「型から考えるユーザー管理」を組み立てていきます。
ユーザー管理の世界観を「型」で決める
まずは共通の土台を作る
ユーザー管理システムでは、どのユーザーにも共通している情報があります。 たとえば、ID・名前・メールアドレスなどです。
これをまず「共通の型」として定義します。
// すべてのユーザーの共通部分
type BaseUser = {
id: number; // ユーザーID
name: string; // 名前
email: string; // メールアドレス
};
TypeScriptRole と Status を型で表現する
次に、ユーザーの「役割」と「状態」を型で表現します。
// ユーザーの役割
type Role = "admin" | "user" | "customer" | "guest";
// ユーザーの状態
type Status = "active" | "inactive" | "banned";
TypeScriptここでLiteral Typeを使うことで、
- 「ありえる役割」
- 「ありえる状態」
を、型としてはっきり決めることができます。
Address型を定義して「住所情報」を切り出す
住所は、複数のユーザーで使い回せる情報なので、 ひとつの型として切り出しておくと再利用しやすくなります。
// 住所情報
type Address = {
postalCode: string; // 郵便番号
prefecture: string; // 都道府県
city: string; // 市区町村
detail?: string; // 番地・建物名(任意)
};
TypeScriptOptional(?)を使って、「番地・建物名は任意」というニュアンスも型に含めています。
User / Admin / Customer / Guest を型で分ける
共通部分 + 役割ごとの違いを組み合わせる
ここでは、BaseUser に役割や住所などを組み合わせて、 それぞれのユーザー種別を型として表現していきます。
// 一般ユーザー
type User = BaseUser & {
role: "user";
status: Status;
};
// 管理者
type Admin = BaseUser & {
role: "admin";
status: Status;
permissions: string[]; // 権限一覧
};
// 顧客ユーザー
type Customer = BaseUser & {
role: "customer";
status: Status;
address?: Address; // 住所は任意
};
// ゲストユーザー(メールアドレスがない場合も想定)
type Guest = {
id: number;
name: string;
role: "guest";
status: Status;
email?: string; // 任意
};
TypeScript重要ポイント(ここはしっかり深掘りしたいところです):
BaseUser & { ... }のように、Intersection Typeで「共通部分 + 役割ごとの違い」を合成しています。roleをLiteral Typeで固定することで、「この型はこの役割」という関係がはっきりします。- Optional(
?)を使って、「必須ではない情報」を自然に表現しています。
すべてのユーザーをまとめる型
ユーザー管理システムでは、「UserかAdminかCustomerかGuestか」のどれかを扱う場面が多いので、 それらをまとめたUnion Typeを用意しておきます。
// すべてのユーザー種別をまとめた型
type AnyUser = User | Admin | Customer | Guest;
TypeScriptユーザー管理の「機能」を型から考える
ここからは、機能ごとに関数を作りながら、 「型がどう安全性を支えてくれるか」を見ていきます。
ユーザー登録(登録)
ここでは、ユーザー一覧を配列で管理するシンプルな形にします。
// ユーザー一覧(メモリ上の簡易DBのイメージ)
let users: AnyUser[] = [];
// ユーザーを登録する関数
function registerUser(user: AnyUser) {
users.push(user);
console.log("ユーザーを登録しました:", user.name, "(", user.role, ")");
}
TypeScriptポイント:
- 引数
userの型がAnyUserなので、「定義済みのユーザー型以外」は登録できません。 - これにより、「よくわからない形のオブジェクト」が紛れ込むことを防げます。
ユーザー検索(検索)
IDでユーザーを検索する関数を作ります。
// IDでユーザーを検索する関数
function findUserById(id: number): AnyUser | undefined {
return users.find((u) => u.id === id);
}
// 使用例
const found = findUserById(1);
if (found) {
console.log("検索結果:", found.name, found.role);
} else {
console.log("ユーザーが見つかりませんでした");
}
TypeScriptポイント:
- 戻り値の型が
AnyUser | undefinedになっていることが重要です。 - 「見つからない可能性がある」ことを型で表現しているので、 呼び出し側は必ず「undefinedかもしれない」ことを意識して分岐を書くようになります。
ユーザー削除(削除)
IDを指定してユーザーを削除する関数です。
// IDでユーザーを削除する関数
function deleteUserById(id: number): boolean {
const beforeLength = users.length;
users = users.filter((u) => u.id !== id);
const afterLength = users.length;
const deleted = beforeLength !== afterLength;
if (deleted) {
console.log("ユーザーを削除しました。ID:", id);
} else {
console.log("削除対象のユーザーが見つかりませんでした。ID:", id);
}
return deleted;
}
TypeScriptポイント:
- 戻り値を
booleanにして、「削除できたかどうか」を明示しています。 - 呼び出し側は、この戻り値を見て「削除成功時の処理」「失敗時の処理」を分けることができます。
権限判定(権限判定)
ここでは、「このユーザーは特定の権限を持っているか?」を判定する関数を作ります。 権限を持つのは Admin だけなので、型の絞り込みが活躍します。
// 特定の権限を持っているかどうかを判定する関数
function hasPermission(user: AnyUser, permission: string): boolean {
if (user.role === "admin") {
// ここでは user は Admin 型に絞り込まれる
return user.permissions.includes(permission);
}
// Admin以外は権限を持たない前提
return false;
}
// 使用例
const adminUser: Admin = {
id: 1,
name: "Admin Taro",
email: "[email protected]",
role: "admin",
status: "active",
permissions: ["manage_users", "view_logs"],
};
registerUser(adminUser);
console.log("ユーザー管理権限:", hasPermission(adminUser, "manage_users")); // true
console.log("ログ閲覧権限:", hasPermission(adminUser, "view_logs")); // true
console.log("削除権限:", hasPermission(adminUser, "delete_system")); // false
TypeScript重要ポイント:
user.role === "admin"のブロック内では、TypeScriptがuserをAdmin型に絞り込んでくれます。- その結果、
permissionsプロパティが安全に使えるようになります。 - 「権限を持つのはAdminだけ」というビジネスルールが、型とロジックの両方に反映されています。
ステータス変更(ステータス変更)
ユーザーの状態(Status)を変更する関数を作ります。
// ユーザーのステータスを変更する関数
function changeUserStatus(id: number, newStatus: Status): void {
const user = findUserById(id);
if (!user) {
console.log("ステータス変更対象のユーザーが見つかりません。ID:", id);
return;
}
// user は AnyUser(Union)ですが、全員が status を持っている前提で設計している
user.status = newStatus;
console.log("ステータスを変更しました:", user.name, "=>", newStatus);
}
TypeScriptポイント:
newStatusの型がStatusなので、「ありえないステータス」は渡せません。- これにより、「typoした文字列」や「仕様にない状態」が紛れ込むことを防げます。
- 「ステータスはこの3種類だけ」というルールが、型として固定されています。
総合練習テンプレート:ユーザー管理システム(第3週版)
最後に、ここまでの内容をひとつのファイルにまとめたテンプレートを載せます。 このままコピーして、ユーザーを追加したり、機能を増やしたりしながら、 「型を使って安全なプログラムを書く」感覚を育ててみてください。
// day21-user-management.ts
// 第3週総合練習:ユーザー管理システム
// 共通ユーザー情報
type BaseUser = {
id: number;
name: string;
email: string;
};
// 役割とステータス
type Role = "admin" | "user" | "customer" | "guest";
type Status = "active" | "inactive" | "banned";
// 住所情報
type Address = {
postalCode: string;
prefecture: string;
city: string;
detail?: string;
};
// ユーザー種別
type User = BaseUser & {
role: "user";
status: Status;
};
type Admin = BaseUser & {
role: "admin";
status: Status;
permissions: string[];
};
type Customer = BaseUser & {
role: "customer";
status: Status;
address?: Address;
};
type Guest = {
id: number;
name: string;
role: "guest";
status: Status;
email?: string;
};
// すべてのユーザー
type AnyUser = User | Admin | Customer | Guest;
// ユーザー一覧
let users: AnyUser[] = [];
// 登録
function registerUser(user: AnyUser) {
users.push(user);
console.log("ユーザーを登録しました:", user.name, "(", user.role, ")");
}
// 検索
function findUserById(id: number): AnyUser | undefined {
return users.find((u) => u.id === id);
}
// 削除
function deleteUserById(id: number): boolean {
const beforeLength = users.length;
users = users.filter((u) => u.id !== id);
const afterLength = users.length;
const deleted = beforeLength !== afterLength;
if (deleted) {
console.log("ユーザーを削除しました。ID:", id);
} else {
console.log("削除対象のユーザーが見つかりませんでした。ID:", id);
}
return deleted;
}
// 権限判定
function hasPermission(user: AnyUser, permission: string): boolean {
if (user.role === "admin") {
return user.permissions.includes(permission);
}
return false;
}
// ステータス変更
function changeUserStatus(id: number, newStatus: Status): void {
const user = findUserById(id);
if (!user) {
console.log("ステータス変更対象のユーザーが見つかりません。ID:", id);
return;
}
user.status = newStatus;
console.log("ステータスを変更しました:", user.name, "=>", newStatus);
}
// テスト用ユーザー作成
const adminUser: Admin = {
id: 1,
name: "Admin Taro",
email: "[email protected]",
role: "admin",
status: "active",
permissions: ["manage_users", "view_logs"],
};
const normalUser: User = {
id: 2,
name: "User Hanako",
email: "[email protected]",
role: "user",
status: "active",
};
const customerUser: Customer = {
id: 3,
name: "Customer Jiro",
email: "[email protected]",
role: "customer",
status: "inactive",
address: {
postalCode: "150-0001",
prefecture: "東京都",
city: "渋谷区",
detail: "神南1-1-1",
},
};
const guestUser: Guest = {
id: 4,
name: "Guest",
role: "guest",
status: "active",
};
// 登録
registerUser(adminUser);
registerUser(normalUser);
registerUser(customerUser);
registerUser(guestUser);
// 検索
const found = findUserById(3);
if (found) {
console.log("検索結果:", found.name, found.role);
}
// 権限判定
console.log("manage_users権限:", hasPermission(adminUser, "manage_users"));
console.log("manage_users権限(一般ユーザー):", hasPermission(normalUser, "manage_users"));
// ステータス変更
changeUserStatus(3, "active");
// 削除
deleteUserById(4);
TypeScriptDay 21のまとめ
Day 21では、
- 共通のユーザー情報を
BaseUserとして切り出す考え方 - Role / Status / Address を型で表現して、「ありえる世界」を固定すること
- User / Admin / Customer / Guest を、Intersection TypeとLiteral Typeで設計する流れ
- 登録・検索・削除・権限判定・ステータス変更といった機能を、型に守られながら書く方法
を、ひとつの「ユーザー管理システム」としてまとめて体験しました。
ここまで来ると、TypeScriptの「型」は単なるおまけではなく、 「仕様をコードの中に刻み込むための道具」として見えてくるはずです。
第3週の総仕上げとして、ぜひ自分なりにフィールドを増やしたり、 機能を追加したりしながら、「型で世界を設計する」感覚を育てていってください。

