はじめてのTypeScriptプログラミング入門 30日コース | TypeScriptの核心「型」 - Day 21:第3週総合練習

30日で身につけるTypeScript
スポンサーリンク
スポンサーリンク

Day 21:第3週総合練習「ユーザー管理システム」を型で設計する

第3週のラストは、ここまで学んできた TypeScriptの「型」 を総動員して、 小さな「ユーザー管理システム」を設計していく総合練習です。

今回のゴールはひとつです。

TypeScriptの「型」を使って、安全なプログラムを書けるようになること。

扱う型と機能は次のとおりです。

  • 型
    • User
    • Admin
    • Customer
    • Guest
    • Role
    • Status
    • Address
  • 機能
    • 登録
    • 検索
    • 削除
    • 権限判定
    • ステータス変更

ここから、ステップバイステップで「型から考えるユーザー管理」を組み立てていきます。

ユーザー管理の世界観を「型」で決める

まずは共通の土台を作る

ユーザー管理システムでは、どのユーザーにも共通している情報があります。 たとえば、ID・名前・メールアドレスなどです。

これをまず「共通の型」として定義します。

// すべてのユーザーの共通部分
type BaseUser = {
  id: number;        // ユーザーID
  name: string;      // 名前
  email: string;     // メールアドレス
};
TypeScript

Role と 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;         // 番地・建物名(任意)
};
TypeScript

Optional(?)を使って、「番地・建物名は任意」というニュアンスも型に含めています。

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);
TypeScript

Day 21のまとめ

Day 21では、

  • 共通のユーザー情報を BaseUser として切り出す考え方
  • Role / Status / Address を型で表現して、「ありえる世界」を固定すること
  • User / Admin / Customer / Guest を、Intersection TypeとLiteral Typeで設計する流れ
  • 登録・検索・削除・権限判定・ステータス変更といった機能を、型に守られながら書く方法

を、ひとつの「ユーザー管理システム」としてまとめて体験しました。

ここまで来ると、TypeScriptの「型」は単なるおまけではなく、 「仕様をコードの中に刻み込むための道具」として見えてくるはずです。

第3週の総仕上げとして、ぜひ自分なりにフィールドを増やしたり、 機能を追加したりしながら、「型で世界を設計する」感覚を育てていってください。

タイトルとURLをコピーしました