はじめてのTypeScriptプログラミング入門 30日コース | Generics・Utility Types・Class・Module - Day 30:卒業制作

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

Day 30:卒業制作 ― 簡易業務管理システムを自分で設計して作る

30日目は、いよいよ「卒業制作」です。 ここまでの学習を総動員して、簡易業務管理システムを自分で設計し、コードに落とし込んでいく練習をします。

テーマの例として、

商品・注文管理システム ユーザー → 商品 → カート → 注文 → 決済 → 注文ステータス

という流れを扱いますが、 大事なのは「何を作るか」よりも どう設計するか です。

ここでは、初心者の方でも追いやすいように、

  • データ構造を考える
  • Type Alias / Interfaceを作る
  • 関数を設計する
  • 配列でデータを管理する
  • 条件分岐を実装する
  • 型エラーを修正する
  • Moduleに分割する
  • コードをリファクタリングする

という流れを、ステップバイステップでかみ砕いていきます。

全体設計をざっくりイメージする

まずは、システム全体の「登場人物」を整理します。

必須型(エンティティ)

  • User:ユーザー情報(顧客・管理者など)
  • Product:商品情報
  • CartItem:カートに入っている商品と数量
  • Order:注文情報(ユーザー・注文アイテム・支払い・ステータスなど)
  • OrderItem:注文内の各商品と数量
  • Payment:支払い情報(方法・金額など)
  • Address:配送先住所
  • Status:注文ステータス(例:"pending" | "paid" | "shipped" | "cancelled")
  • Role:ユーザーのロール(例:"customer" | "admin")

必須機能(ざっくりしたユースケース)

  • ユーザー登録
  • 商品登録
  • 商品検索
  • カート操作(追加・削除)
  • 注文作成
  • 注文検索
  • ステータス変更
  • データ削除
  • エラー処理(存在しないIDなど)

ここから、型 → 関数 → モジュールの順に組み立てていきます。

Step 1:types.ts ― データ構造をType Alias / Interfaceで定義する

まずは、すべての基本型を types.ts にまとめます。 ここでは、Type AliasとInterfaceを両方使ってみます。

// types.ts
// 簡易業務管理システムで使う型定義をまとめるファイル

// ユーザーのロール(権限)
export type Role = "customer" | "admin";

// 注文ステータス
export type Status = "pending" | "paid" | "shipped" | "cancelled";

// 支払い方法
export type PaymentMethod = "credit_card" | "bank_transfer" | "cash";

// ユーザー情報
export interface User {
  id: number;
  name: string;
  email: string;
  role: Role;
}

// 商品情報
export interface Product {
  id: number;
  name: string;
  price: number;
  stock: number; // 在庫数
}

// カートアイテム
export interface CartItem {
  productId: number;
  quantity: number;
}

// 注文内の各商品
export interface OrderItem {
  productId: number;
  quantity: number;
  unitPrice: number; // 注文時の単価
}

// 住所情報
export interface Address {
  postalCode: string;
  prefecture: string;
  city: string;
  detail: string;
}

// 支払い情報
export interface Payment {
  method: PaymentMethod;
  amount: number;
  paidAt: Date | null; // 支払い日時(未払いならnull)
}

// 注文情報
export interface Order {
  id: number;
  userId: number;
  items: OrderItem[];
  shippingAddress: Address;
  payment: Payment;
  status: Status;
  createdAt: Date;
}
TypeScript

重要ポイント:

  • Role / Status / PaymentMethod は Union Type で、 取り得る値を限定しています。これにより、誤入力を防ぎやすくなります。
  • User / Product / Order などは Interface で定義し、 「業務データの構造」をはっきりさせています。
  • OrderItem に unitPrice を持たせることで、 「注文時の価格」を記録できるようにしています(後から価格が変わっても履歴が保てます)。

Step 2:userRepository.ts ― ユーザー登録・検索を設計する

次に、ユーザー関連の機能を userRepository.ts にまとめます。

// userRepository.ts
// ユーザー登録・検索などを担当するモジュール

import { User, Role } from "./types";

let users: User[] = [];
let nextUserId = 1;

// ユーザー登録
export function registerUser(
  name: string,
  email: string,
  role: Role = "customer"
): User {
  const newUser: User = {
    id: nextUserId++,
    name,
    email,
    role,
  };
  users.push(newUser);
  return newUser;
}

// メールアドレスでユーザーを検索
export function findUserByEmail(email: string): User | null {
  const user = users.find((u) => u.email === email);
  return user ?? null;
}

// IDでユーザーを取得
export function getUserById(id: number): User | null {
  const user = users.find((u) => u.id === id);
  return user ?? null;
}

// 全ユーザー一覧
export function getAllUsers(): User[] {
  return [...users];
}
TypeScript

ポイント:

  • 配列 users でデータを管理し、nextUserId でIDを自動採番しています。
  • findUserByEmail / getUserById は、存在しない場合に null を返すようにして、 呼び出し側で エラー処理 をしやすくしています。

Step 3:productRepository.ts ― 商品登録・検索を設計する

次に、商品関連の機能を productRepository.ts にまとめます。

// productRepository.ts
// 商品登録・検索・在庫管理を担当するモジュール

import { Product } from "./types";

let products: Product[] = [];
let nextProductId = 1;

// 商品登録
export function addProduct(
  name: string,
  price: number,
  stock: number
): Product {
  const newProduct: Product = {
    id: nextProductId++,
    name,
    price,
    stock,
  };
  products.push(newProduct);
  return newProduct;
}

// 商品検索(名前にキーワードを含む)
export function searchProducts(keyword: string): Product[] {
  const lower = keyword.toLowerCase();
  return products.filter((p) => p.name.toLowerCase().includes(lower));
}

// IDで商品取得
export function getProductById(id: number): Product | null {
  const product = products.find((p) => p.id === id);
  return product ?? null;
}

// 全商品一覧
export function getAllProducts(): Product[] {
  return [...products];
}

// 在庫を減らす(注文時に使用)
export function reduceStock(productId: number, quantity: number): boolean {
  const product = products.find((p) => p.id === productId);
  if (!product) {
    return false; // 商品が存在しない
  }
  if (product.stock < quantity) {
    return false; // 在庫不足
  }
  product.stock -= quantity;
  return true;
}
TypeScript

重要ポイント:

  • searchProducts で 検索機能 を実装しています。
  • reduceStock で在庫を減らす際、
    • 商品が存在しない
    • 在庫が足りない というケースを条件分岐でチェックし、boolean で成功・失敗を返しています。 これが エラー処理の入り口 になります。

Step 4:cartService.ts ― カート機能を設計する

次に、カート機能を cartService.ts にまとめます。 ここでは、ユーザーごとにカートを持つイメージで設計します。

// cartService.ts
// カート操作(追加・削除・取得)を担当するモジュール

import { CartItem } from "./types";

type UserCart = {
  userId: number;
  items: CartItem[];
};

let carts: UserCart[] = [];

// ユーザーのカートを取得(なければ新規作成)
function getOrCreateCart(userId: number): UserCart {
  let cart = carts.find((c) => c.userId === userId);
  if (!cart) {
    cart = { userId, items: [] };
    carts.push(cart);
  }
  return cart;
}

// カートに商品を追加
export function addToCart(userId: number, productId: number, quantity: number): void {
  const cart = getOrCreateCart(userId);

  const existingItem = cart.items.find((item) => item.productId === productId);
  if (existingItem) {
    // すでに同じ商品がある場合は数量を加算
    existingItem.quantity += quantity;
  } else {
    cart.items.push({ productId, quantity });
  }
}

// カートから商品を削除
export function removeFromCart(userId: number, productId: number): void {
  const cart = getOrCreateCart(userId);
  cart.items = cart.items.filter((item) => item.productId !== productId);
}

// カートを空にする
export function clearCart(userId: number): void {
  const cart = getOrCreateCart(userId);
  cart.items = [];
}

// カート内容を取得
export function getCartItems(userId: number): CartItem[] {
  const cart = getOrCreateCart(userId);
  return [...cart.items];
}
TypeScript

ポイント:

  • UserCart という内部用の型を作り、ユーザーごとのカートを管理しています。
  • getOrCreateCart で「なければ作る」というパターンをまとめておくことで、 他の関数がシンプルになります(リファクタリングの一例です)。

Step 5:orderService.ts ― 注文作成・検索・ステータス変更・エラー処理

次に、注文関連の機能を orderService.ts にまとめます。 ここが今回のシステムの「心臓部」です。

// orderService.ts
// 注文作成・検索・ステータス変更・エラー処理を担当するモジュール

import {
  Order,
  OrderItem,
  Address,
  Payment,
  Status,
  PaymentMethod,
} from "./types";
import { getCartItems, clearCart } from "./cartService";
import { getUserById } from "./userRepository";
import { getProductById, reduceStock } from "./productRepository";

let orders: Order[] = [];
let nextOrderId = 1;

// 注文作成
export function createOrder(
  userId: number,
  shippingAddress: Address,
  paymentMethod: PaymentMethod
): Order {
  // ユーザー存在チェック
  const user = getUserById(userId);
  if (!user) {
    throw new Error("ユーザーが存在しません");
  }

  // カート取得
  const cartItems = getCartItems(userId);
  if (cartItems.length === 0) {
    throw new Error("カートが空です");
  }

  // カートからOrderItemを作成しつつ、在庫を減らす
  const orderItems: OrderItem[] = [];
  let totalAmount = 0;

  for (const cartItem of cartItems) {
    const product = getProductById(cartItem.productId);
    if (!product) {
      throw new Error(`商品ID ${cartItem.productId} が存在しません`);
    }

    // 在庫チェック&減少
    const ok = reduceStock(product.id, cartItem.quantity);
    if (!ok) {
      throw new Error(`商品「${product.name}」の在庫が不足しています`);
    }

    const item: OrderItem = {
      productId: product.id,
      quantity: cartItem.quantity,
      unitPrice: product.price,
    };
    orderItems.push(item);

    totalAmount += product.price * cartItem.quantity;
  }

  // 支払い情報
  const payment: Payment = {
    method: paymentMethod,
    amount: totalAmount,
    paidAt: null, // ここでは未払いとして扱う
  };

  const newOrder: Order = {
    id: nextOrderId++,
    userId,
    items: orderItems,
    shippingAddress,
    payment,
    status: "pending",
    createdAt: new Date(),
  };

  orders.push(newOrder);

  // カートを空にする
  clearCart(userId);

  return newOrder;
}

// 注文検索(ユーザーIDで絞り込み)
export function getOrdersByUser(userId: number): Order[] {
  return orders.filter((order) => order.userId === userId);
}

// 注文IDで取得
export function getOrderById(orderId: number): Order | null {
  const order = orders.find((o) => o.id === orderId);
  return order ?? null;
}

// ステータス変更
export function changeOrderStatus(orderId: number, status: Status): Order | null {
  const order = orders.find((o) => o.id === orderId);
  if (!order) {
    return null;
  }
  order.status = status;
  return order;
}

// 注文削除
export function deleteOrder(orderId: number): boolean {
  const beforeLength = orders.length;
  orders = orders.filter((o) => o.id !== orderId);
  const afterLength = orders.length;
  return afterLength < beforeLength;
}
TypeScript

ここでの重要ポイント:

  • createOrder で、エラー処理をしっかり書いています。
    • ユーザーが存在しない
    • カートが空
    • 商品が存在しない
    • 在庫不足 などのケースで throw new Error(...) を使っています。
  • changeOrderStatus は、存在しない注文IDの場合に null を返し、 呼び出し側で「ステータス変更に失敗した」ことを検知できるようにしています。

Step 6:main.ts ― 全体の流れを組み立てて動かす

最後に、main.ts で全モジュールを組み合わせて、 簡易業務管理システムの流れをコンソール上で体験します。

// main.ts
// 簡易業務管理システムのエントリーポイント

import { Address } from "./types";
import { registerUser, getAllUsers } from "./userRepository";
import { addProduct, getAllProducts, searchProducts } from "./productRepository";
import { addToCart, getCartItems } from "./cartService";
import {
  createOrder,
  getOrdersByUser,
  changeOrderStatus,
  deleteOrder,
} from "./orderService";

// 住所のサンプル
const sampleAddress: Address = {
  postalCode: "123-4567",
  prefecture: "東京都",
  city: "渋谷区",
  detail: "道玄坂1-2-3",
};

// 一覧表示用のヘルパー
function printUsers(): void {
  console.log("\n=== ユーザー一覧 ===");
  const users = getAllUsers();
  if (users.length === 0) {
    console.log("ユーザーはまだ登録されていません");
    return;
  }
  users.forEach((u) => {
    console.log(`ID: ${u.id} / 名前: ${u.name} / ロール: ${u.role}`);
  });
}

function printProducts(): void {
  console.log("\n=== 商品一覧 ===");
  const products = getAllProducts();
  if (products.length === 0) {
    console.log("商品はまだ登録されていません");
    return;
  }
  products.forEach((p) => {
    console.log(
      `ID: ${p.id} / 商品名: ${p.name} / 価格: ${p.price} / 在庫: ${p.stock}`
    );
  });
}

function printCart(userId: number): void {
  console.log(`\n=== ユーザーID ${userId} のカート ===`);
  const items = getCartItems(userId);
  if (items.length === 0) {
    console.log("カートは空です");
    return;
  }
  items.forEach((item) => {
    console.log(`商品ID: ${item.productId} / 数量: ${item.quantity}`);
  });
}

function printOrders(userId: number): void {
  console.log(`\n=== ユーザーID ${userId} の注文一覧 ===`);
  const orders = getOrdersByUser(userId);
  if (orders.length === 0) {
    console.log("注文はまだありません");
    return;
  }
  orders.forEach((order) => {
    console.log(
      `注文ID: ${order.id} / ステータス: ${order.status} / 金額: ${order.payment.amount}`
    );
  });
}

// 実行例
function main(): void {
  console.log("=== 簡易業務管理システム(卒業制作) ===");

  // ユーザー登録
  const user = registerUser("Taro", "[email protected]", "customer");
  const admin = registerUser("Admin", "[email protected]", "admin");
  printUsers();

  // 商品登録
  const p1 = addProduct("ノートPC", 120000, 10);
  const p2 = addProduct("マウス", 2000, 50);
  const p3 = addProduct("キーボード", 5000, 30);
  printProducts();

  // 商品検索
  const searchResult = searchProducts("マウス");
  console.log("\n=== 「マウス」で商品検索 ===");
  searchResult.forEach((p) => {
    console.log(`ID: ${p.id} / 商品名: ${p.name}`);
  });

  // カート操作
  addToCart(user.id, p1.id, 1); // ノートPCを1台
  addToCart(user.id, p2.id, 2); // マウスを2個
  printCart(user.id);

  // 注文作成(エラー処理付き)
  try {
    const order = createOrder(user.id, sampleAddress, "credit_card");
    console.log("\n=== 注文作成成功 ===");
    console.log(`注文ID: ${order.id} / 金額: ${order.payment.amount}`);
  } catch (error) {
    console.error("\n=== 注文作成エラー ===");
    console.error((error as Error).message);
  }

  // 注文一覧表示
  printOrders(user.id);

  // ステータス変更
  const orders = getOrdersByUser(user.id);
  if (orders.length > 0) {
    const firstOrder = orders[0];
    const updated = changeOrderStatus(firstOrder.id, "paid");
    if (updated) {
      console.log("\n=== ステータス変更 ===");
      console.log(`注文ID: ${updated.id} のステータスを ${updated.status} に変更しました`);
    }
  }

  // 注文削除
  if (orders.length > 0) {
    const firstOrder = orders[0];
    const deleted = deleteOrder(firstOrder.id);
    console.log("\n=== 注文削除 ===");
    console.log(
      deleted
        ? `注文ID: ${firstOrder.id} を削除しました`
        : `注文ID: ${firstOrder.id} の削除に失敗しました`
    );
  }

  // 最終的な注文一覧
  printOrders(user.id);
}

// エントリーポイント
main();
TypeScript

ここでの重要ポイント:

  • try { createOrder(...) } catch (error) { ... } で、 注文作成時のエラー(在庫不足・ユーザー不在など)を受け止めています。
  • 各種 print... 関数で、 「配列で管理しているデータ」を分かりやすく表示しています。
  • 全体の流れとして、
    • ユーザー登録
    • 商品登録
    • 商品検索
    • カート操作
    • 注文作成
    • 注文検索
    • ステータス変更
    • データ削除 を一通り体験できるようになっています。

リファクタリングの視点を少しだけ

卒業制作らしく、最後に「リファクタリング」の視点も触れておきます。

  • 共通処理をまとめる
    • 例:エラーメッセージのフォーマット関数を作る
    • 例:print... 系の表示ロジックを別モジュールに切り出す
  • 型を再利用しやすくする
    • 例:UserCart を types.ts に移動して、他のモジュールでも使えるようにする
  • Utility Typesをさらに活用する
    • 例:Order から OrderSummary を Pick で作る
  • Moduleを増やして責務を分ける
    • 例:logging.ts を作ってログ出力をまとめる
    • 例:validation.ts を作って入力チェックをまとめる

こうした小さな改善を積み重ねていくことで、 「業務システムっぽいコード」に近づいていきます。

Day 30のまとめ ― 自分で設計して作る力

Day 30では、

  • 業務データの構造をType Alias / Interfaceで設計する
  • ユーザー・商品・カート・注文・決済・ステータスという流れをコードに落とし込む
  • 配列でデータを管理し、条件分岐でエラー処理を行う
  • Moduleに分割して、責務ごとにコードを整理する
  • リファクタリングの入り口に触れる

という「総合実践」を通して、 TypeScriptで小さな業務システムを自分の手で組み立てる感覚を味わっていただきました。

ここまで30日間で積み上げてきたものは、 そのまま「次の一歩」の土台になります。

  • もう少し複雑なドメイン(在庫管理・予約システムなど)に挑戦する
  • 非同期処理や永続化(DB・ローカルストレージ)を組み合わせる
  • フロントエンドフレームワーク(Reactなど)とTypeScriptを組み合わせる

など、どこに進んでも、 今回の卒業制作で触れた「設計してから作る」という感覚が、 きっと支えになってくれるはずです。

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