Logo Image

Blog Article

TypeScript オブジェクト指向ってなに?なぜ必要なの?

00

はじめに

プログラミングを学び始めてしばらく経つと、よく耳にするのが「オブジェクト指向プログラミング(OOP)」という言葉です。
最初は「クラス?継承?インスタンス化?」といった専門用語に戸惑い、「なんのためにこれが必要なのか?」と疑問を感じる人も多いのではないでしょうか。

私もその一人でした。

この記事では、TypeScript を使いながら、オブジェクト指向とは何か、なぜ使うのか、どうやって使うのかを少しずつ丁寧に解説していきます。

この記事では、クラスの基本から応用的な機能まで、初学者にも分かりやすく解説します。

クラスとインスタンス化の基本

クラスは「オブジェクトの設計図」として機能します。実際のオブジェクト(インスタンス)を作るには、newキーワードを使用します。

class Car {
  // コンストラクタ: インスタンス生成時に自動実行される
  constructor(public brand: string) {
    // thisは現在作成しているオブジェクト(インスタンス)を指す
    this.brand = brand;
  }

  display(): void {
    console.log(`This car is a ${this.brand}`);
  }
}

// newキーワードでインスタンスを生成
let myCar = new Car("Toyota");
myCar.display(); // "This car is a Toyota"

インスタンス化の流れ

  1. メモリ確保: newキーワードで新しいオブジェクトがメモリ上に作成される
  2. 初期化: コンストラクタが実行され、プロパティが設定される
  3. アクセス可能: インスタンスのプロパティやメソッドにアクセスできるようになる

アクセス修飾子でプロパティを管理

TypeScriptには3つのアクセス修飾子があり、プロパティやメソッドのアクセス範囲を制御できます。

class Vehicle {
  public make: string;        // どこからでもアクセス可能
  protected year: number;     // クラス内と派生クラスでアクセス可能
  private model: string;      // クラス内のみアクセス可能

  constructor(make: string, model: string, year: number) {
    this.make = make;
    this.model = model;
    this.year = year;
  }

  public displayMake(): void {
    console.log(`This vehicle is made by: ${this.make}`);
  }

  protected displayYear(): void {
    console.log(`This vehicle was made in: ${this.year}`);
  }

  private displayModel(): void {
    console.log(`This vehicle model is: ${this.model}`);
  }

  public showModel(): void {
    this.displayModel(); // クラス内からprivateメソッドにアクセス
  }
}

class Car2 extends Vehicle {
  constructor(make: string, model: string, year: number) {
    super(make, model, year);
  }

  displayInfo(): void {
    this.displayMake();  // publicメソッドにアクセス
    this.displayYear();  // protectedメソッドにアクセス
    // this.displayModel(); // ❌ privateメソッドにはアクセス不可
  }
}

let myCar2 = new Car2("Toyota", "Corolla", 2025);
myCar2.displayInfo();
myCar2.displayMake();  // publicメソッドにアクセス
myCar2.showModel();

アクセス修飾子の使い分け

  • public: 外部からも自由にアクセスしたいプロパティ・メソッド
  • protected: 継承先のクラスでも使いたいが、外部には公開したくないもの
  • private: そのクラス内でのみ使用するもの

Getter/Setterで安全なプロパティアクセス

Getter/Setterを使うことで、プロパティへのアクセスを制御し、バリデーションを行えます。

class Person {
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  // get: プロパティを外部から読み取る
  get name(): string {
    return this._name;
  }

  // set: プロパティに新しい値を設定
  set name(value: string) {
    if (value.length > 0) {
      this._name = value;
    } else {
      console.error("Invalid name: 名前は1文字以上である必要があります");
    }
  }
}

let person = new Person("John");
console.log(person.name); // get name()が呼ばれる → "John"
person.name = "Jane";     // set name()が呼ばれ、_nameが変更される
console.log(person.name); // "Jane"

person.name = "";         // "Invalid name: 名前は1文字以上である必要があります"

Getter/Setterのメリット・デメリット

メリット:
  • プロパティへのアクセスを細かく制御できる
  • 値の設定時にバリデーションを行える
  • プロパティアクセス時に追加処理を実行できる
デメリット:
  • コードが冗長になりがち
  • デバッグが難しくなる場合がある

Getter/Setterを使わない場合

class Person2 {
  public name: string;

  constructor(name: string) {
    this.name = name;
  }
}

let person2 = new Person2("John");
console.log(person2.name);
person2.name = "Jane";
person2.name = ""; // ❌ 空文字でも設定できてしまう

Getter/Setterを使わない場合、プロパティを自由に変更できるため、不正な値が設定される可能性があります。

readonly修飾子で変更を防ぐ

readonly修飾子を使うと、初期化後にプロパティが変更されることを防げます。

なぜreadonly修飾子が必要なのか?

プログラミングでは、一度設定したら変更してはいけないデータがあります。

例えば:

  • 商品の製造番号
  • ユーザーのID
  • 設定ファイルの値

これらを間違って変更してしまうと、システム全体に深刻な問題を引き起こす可能性があります。readonly修飾子は、そうした事故を防ぐための「安全装置」なのです。

いつ使うのか?

  • 設定値: アプリケーションの設定値など、起動時に決まったら変更してはいけない値
  • 識別子: ユーザーID、商品コードなど、一意性を保つ必要がある値
  • 計算結果: 一度計算したら変更する必要のない値

メリット

  • バグの予防: 変更してはいけない値を誤って変更することを防ぐ
  • 意図の明確化: このプロパティは変更しないということがコードで明示される
  • 安心感: チーム開発で他の人が間違って変更する心配がない

例1:

class Car {
  readonly make: string;
  readonly model: string;
  readonly year: number;

  constructor(make: string, model: string, year: number) {
    this.make = make;
    this.model = model;
    this.year = year;
  }

  displayDetails(): void {
    console.log(`Car: ${this.make} ${this.model}, Year: ${this.year}`);
  }
}

const myCar = new Car("Toyota", "Prius", 2025);
myCar.displayDetails(); // "Car: Toyota Prius, Year: 2025"

// myCar.make = "Honda"; // ❌ コンパイルエラー: readonlyプロパティは変更不可

応用例:

class BankAccount {
  // 口座番号は開設時に決まったら変更できない
  readonly accountNumber: string;
  // 口座開設日も変更できない
  readonly openedDate: Date;
  // 残高は変更可能(入出金があるため)
  private balance: number;

  constructor(accountNumber: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.openedDate = new Date();
    this.balance = initialBalance;
  }

  // 残高確認(readonlyプロパティは安全に参照できる)
  getAccountInfo(): string {
    return `口座番号: ${this.accountNumber}, 開設日: ${this.openedDate.toDateString()}`;
  }

  // 入金処理
  deposit(amount: number): void {
    this.balance += amount;
  }
}

const account = new BankAccount("12345-67890", 1000);
console.log(account.accountNumber); // ✅ 読み取りは可能
// account.accountNumber = "99999-88888"; // ❌ コンパイルエラー!変更は不可

staticメンバで共通機能を管理

static修飾子を使うと、インスタンスを作成せずにクラス名から直接アクセスできるメンバを定義できます。

なぜstaticが必要なのか?

時には、個々のインスタンス(オブジェクト)ではなく、クラス全体で共通して使いたい機能があります。

毎回インスタンスを作るのは無駄ですし、共通の値は一箇所で管理したいものです。

例えば:

  • 数学的な計算(円周率、計算関数)
  • アプリケーション全体の設定
  • ユーティリティ関数

いつ使うのか?

  • 定数: 円周率、税率など、アプリケーション全体で使う固定値
  • ユーティリティ関数: 文字列操作、日付操作など、どこでも使える汎用的な関数
  • カウンター: 作成されたインスタンスの数をカウントしたい場合

staticメンバのメリット・デメリット

メリット:
  • インスタンス作成せずにメンバにアクセス可能
  • すべてのインスタンスで共通の値やメソッドを保持
  • ユーティリティクラスや定数の管理に適している
デメリット:
  • インスタンスごとの状態を保持できない
  • 多用するとクラスの責務が増大し、保守が困難になる

例1:

class MathUtils {
  static PI: number = 3.14159;

  static add(x: number, y: number): number {
    return x + y;
  }

  static subtract(x: number, y: number): number {
    return x - y;
  }

  static circleArea(radius: number): number {
    return this.PI * radius * radius;
  }
}

// インスタンス作成不要でアクセス
console.log(MathUtils.PI);              // 3.14159
console.log(MathUtils.add(10, 5));      // 15
console.log(MathUtils.subtract(10, 5)); // 5
console.log(MathUtils.circleArea(5));   // 78.53975

応用例:

class User {
  // すべてのUserインスタンスで共通のカウンター
  private static userCount: number = 0;
  // アプリケーション設定(全ユーザー共通)
  static readonly MAX_LOGIN_ATTEMPTS: number = 3;

  private id: number;
  private name: string;
  private loginAttempts: number = 0;

  constructor(name: string) {
    // インスタンス作成のたびにカウンターを増やす
    User.userCount++;
    this.id = User.userCount;
    this.name = name;
  }

  // staticメソッド:インスタンスを作らずに呼び出せる
  static getTotalUsers(): number {
    return User.userCount;
  }

  // staticメソッド:ユーザー名の妥当性チェック
  static isValidUserName(name: string): boolean {
    return name.length >= 3 && name.length <= 20;
  }

  // 通常のメソッド
  login(password: string): boolean {
    if (this.loginAttempts >= User.MAX_LOGIN_ATTEMPTS) {
      console.log("ログイン試行回数上限に達しました");
      return false;
    }

    // パスワード検証処理(簡略化)
    if (password === "correct") {
      this.loginAttempts = 0;
      return true;
    } else {
      this.loginAttempts++;
      return false;
    }
  }
}

// 使用例
console.log(User.getTotalUsers()); // 0

// ユーザー名チェック(インスタンスを作る前に確認できる)
if (User.isValidUserName("太郎")) {
  const user1 = new User("太郎");
  const user2 = new User("花子");

  console.log(User.getTotalUsers()); // 2
  console.log(User.MAX_LOGIN_ATTEMPTS); // 3
}

クラスの継承で機能を拡張

継承を使うことで、既存のクラスの機能を引き継ぎつつ、新しい機能を追加できます。

なぜ継承が必要なのか?

実際の開発では、似たような機能を持つクラスを複数作ることがよくあります。

毎回同じコードを書き直すのは非効率ですし、共通部分の修正が大変になります。継承を使えば、共通部分を一箇所にまとめ、違う部分だけを個別に定義できます。

例えば:

  • 社員、アルバイト、派遣社員(全員「人」だが、少しずつ違う)
  • 犬、猫、鳥(全員「動物」だが、鳴き方が違う)

いつ使うのか?

  • 共通の属性・機能: 複数のクラスで同じプロパティやメソッドが必要な場合
  • 段階的な専門化: 基本的な機能から、より具体的な機能へと段階的に特化させたい場合
  • コードの再利用: 既存のクラスの機能を活用しつつ、新しい機能を追加したい場合

継承のメリット・デメリット

メリット:
  • 既存の機能を再利用しつつ新機能を追加
  • 親クラスの修正がすべての子クラスに適用される
デメリット:
  • 親クラスの変更が子クラスに影響を及ぼす
  • 継承が深くなりすぎると管理が困難

例1:

class Animal {
  constructor(public name: string) {}

  move(distance: number): void {
    console.log(`${this.name} moved ${distance}m.`);
  }

  eat(): void {
    console.log(`${this.name} is eating.`);
  }
}

class Snake extends Animal {
  move(distance = 5): void {
    console.log("Slithering...");
    super.move(distance); // 親クラスのメソッドを呼び出し
  }

  shed(): void {
    console.log(`${this.name} is shedding skin.`);
  }
}

class Horse extends Animal {
  move(distance = 45): void {
    console.log("Galloping...");
    super.move(distance);
  }

  neigh(): void {
    console.log(`${this.name} says neigh!`);
  }
}

const snake = new Snake("Python");
snake.move();    // "Slithering..." → "Python moved 5m."
snake.eat();     // "Python is eating."
snake.shed();    // "Python is shedding skin."

const horse = new Horse("Thunder");
horse.move();    // "Galloping..." → "Thunder moved 45m."
horse.neigh();   // "Thunder says neigh!"

応用例:

// 基底クラス:すべての従業員に共通する機能
class Employee {
  protected name: string;  // protectedで子クラスからアクセス可能
  protected id: string;
  protected baseSalary: number;

  constructor(name: string, id: string, baseSalary: number) {
    this.name = name;
    this.id = id;
    this.baseSalary = baseSalary;
  }

  // 共通メソッド:自己紹介
  introduce(): string {
    return `こんにちは、${this.name}です。ID: ${this.id}`;
  }

  // 基本給与計算(子クラスでオーバーライド可能)
  calculateSalary(): number {
    return this.baseSalary;
  }

  // 共通メソッド:勤務状況表示
  getWorkStatus(): string {
    return `${this.name}さんの月給: ${this.calculateSalary()}円`;
  }
}

// 正社員クラス:基本給+ボーナス
class FullTimeEmployee extends Employee {
  private bonus: number;

  constructor(name: string, id: string, baseSalary: number, bonus: number) {
    super(name, id, baseSalary); // 親クラスのコンストラクタを呼び出し
    this.bonus = bonus;
  }

  // 給与計算をオーバーライド(ボーナス込み)
  calculateSalary(): number {
    return this.baseSalary + this.bonus;
  }

  // 正社員独自のメソッド
  getAnnualSalary(): number {
    return this.calculateSalary() * 12;
  }
}

// アルバイトクラス:時給×労働時間
class PartTimeEmployee extends Employee {
  private hourlyWage: number;
  private hoursWorked: number;

  constructor(name: string, id: string, hourlyWage: number, hoursWorked: number) {
    super(name, id, 0); // 基本給は0
    this.hourlyWage = hourlyWage;
    this.hoursWorked = hoursWorked;
  }

  // 給与計算をオーバーライド(時給計算)
  calculateSalary(): number {
    return this.hourlyWage * this.hoursWorked;
  }

  // アルバイト独自のメソッド
  addWorkHours(hours: number): void {
    this.hoursWorked += hours;
  }
}

// 使用例
const fullTimeEmp = new FullTimeEmployee("田中太郎", "FT001", 300000, 50000);
const partTimeEmp = new PartTimeEmployee("佐藤花子", "PT001", 1200, 80);

console.log(fullTimeEmp.introduce());  // 共通メソッドを使用
console.log(fullTimeEmp.getWorkStatus()); // "田中太郎さんの月給: 350000円"
console.log(fullTimeEmp.getAnnualSalary()); // 正社員独自メソッド

console.log(partTimeEmp.introduce());  // 同じ共通メソッドを使用
console.log(partTimeEmp.getWorkStatus()); // "佐藤花子さんの月給: 96000円"
partTimeEmp.addWorkHours(10); // アルバイト独自メソッド

インターフェースで型安全性を向上

インターフェースを使用すると、オブジェクトの構造を定義でき、型の安全性が向上します。

なぜインターフェースが必要なのか?

チーム開発では、「このオブジェクトにはどんなプロパティがあるのか?」「この関数は何を返すのか?」を明確にする必要があります。インターフェースは、オブジェクトの「形」を定義する契約書のような役割を果たします。

いつ使うのか?

  • API設計: 関数の引数や戻り値の構造を明確にしたい場合
  • データモデル: データベースから取得するデータの形を定義したい場合
  • コンポーネント間の連携: 異なる部分で同じ形のデータをやり取りしたい場合

インターフェースのメリット

  • 型の安全性を向上させ、誤った値の代入を防ぐ
  • 関数の戻り値の構造が明確になる
  • オブジェクトの契約を明示的に定義

例1:

interface Greeting {
  message: string;
  sender: string;
  timestamp?: Date; // オプショナルプロパティ
}

function getGreeting(name: string): Greeting {
  return { 
    message: `Hello, ${name}!`, 
    sender: "TypeScript App",
    timestamp: new Date()
  };
}

const greeting = getGreeting("World");
console.log(greeting.message);   // "Hello, World!"
console.log(greeting.sender);    // "TypeScript App"
console.log(greeting.timestamp); // 現在の日時

// 型チェックにより、存在しないプロパティへのアクセスを防ぐ
// console.log(greeting.unknownProp); // ❌ コンパイルエラー

応用例:

// 商品の基本情報インターフェース
interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}

// 商品レビューのインターフェース
interface Review {
  reviewId: string;
  productId: string;
  customerName: string;
  rating: number; // 1-5の評価
  comment: string;
  reviewDate: Date;
}

// 注文情報のインターフェース
interface Order {
  orderId: string;
  customerId: string;
  products: Product[];
  totalAmount: number;
  orderDate: Date;
  status: 'pending' | 'shipped' | 'delivered' | 'cancelled';
}

// 商品管理クラス
class ProductManager {
  private products: Product[] = [];

  // インターフェースを使って引数の型を明確に
  addProduct(product: Product): void {
    this.products.push(product);
    console.log(`商品「${product.name}」を追加しました`);
  }

  // 戻り値の型も明確に
  getProduct(id: string): Product | null {
    return this.products.find(p => p.id === id) || null;
  }

  // 複数の商品を返す場合
  getProductsByCategory(category: string): Product[] {
    return this.products.filter(p => p.category === category);
  }

  // 在庫ありの商品のみを返す
  getAvailableProducts(): Product[] {
    return this.products.filter(p => p.inStock);
  }
}

// 注文管理クラス
class OrderManager {
  private orders: Order[] = [];

  // インターフェースを使って複雑なオブジェクトも型安全に
  createOrder(customerId: string, products: Product[]): Order {
    const totalAmount = products.reduce((sum, product) => sum + product.price, 0);

    const order: Order = {
      orderId: `ORD-${Date.now()}`,
      customerId,
      products,
      totalAmount,
      orderDate: new Date(),
      status: 'pending'
    };

    this.orders.push(order);
    return order;
  }

  // ステータス更新も型安全に
  updateOrderStatus(orderId: string, status: Order['status']): boolean {
    const order = this.orders.find(o => o.orderId === orderId);
    if (order) {
      order.status = status;
      return true;
    }
    return false;
  }
}

// 使用例
const productManager = new ProductManager();
const orderManager = new OrderManager();

// 商品追加(インターフェースの形に合わせる必要がある)
const laptop: Product = {
  id: "PROD-001",
  name: "ノートパソコン",
  price: 89800,
  category: "電子機器",
  inStock: true
};

const smartphone: Product = {
  id: "PROD-002",
  name: "スマートフォン",
  price: 67800,
  category: "電子機器",
  inStock: true
};

productManager.addProduct(laptop);
productManager.addProduct(smartphone);

// 注文作成
const products = [laptop, smartphone];
const order = orderManager.createOrder("CUST-001", products);
console.log(`注文総額: ${order.totalAmount}円`);

// ステータス更新(定義された値以外はエラーになる)
orderManager.updateOrderStatus(order.orderId, "shipped"); // ✅ OK
// orderManager.updateOrderStatus(order.orderId, "invalid"); // ❌ コンパイルエラー

ユーティリティ型で柔軟な型操作

TypeScriptには、既存の型を柔軟に操作できるユーティリティ型が用意されています。

なぜユーティリティ型が必要なのか?

実際の開発では、既存の型をベースに「少しだけ違う型」が必要になることがよくあります。

全部新しく定義するのは大変ですし、元の型が変更されたときの追従も困難です。ユーティリティ型を使えば、既存の型を「加工」して新しい型を作れます。

例えば:

  • 「更新時は一部のフィールドだけ変更したい」
  • 「表示用には読み取り専用にしたい」
  • 「設定ファイルでは一部フィールドが必須」

いつ使うのか?

  • CRUD操作: 作成・更新・表示で微妙に異なる型が必要な場合
  • フォーム処理: 入力フォームと表示用で異なる型が必要な場合
  • API設計: リクエストとレスポンスで微妙に異なる型が必要な場合
interface User {
  name: string;
  age: number;
  email: string;
}

Partial<T> - すべてのプロパティをオプショナルに

type PartialUser = Partial<User>;

const user1: PartialUser = {}; // すべてオプショナル
const user2: PartialUser = { name: "Kakuta" };
const user3: PartialUser = { age: 28, email: "test@example.com" };

// 部分的な更新に便利
function updateUser(id: number, updates: Partial<User>): void {
  // 指定されたプロパティのみ更新
  console.log(`Updating user ${id} with:`, updates);
}

updateUser(1, { name: "New Name" }); // nameのみ更新

Required<T> - すべてのプロパティを必須に

interface OptionalUser {
  name?: string;
  age?: number;
  email?: string;
}

type RequiredUser = Required<OptionalUser>;

const user4: RequiredUser = { 
  name: "Kakuta", 
  age: 28,
  email: "kakuta@example.com" // すべて必須
};

Readonly<T> - すべてのプロパティを読み取り専用に

const user5: Readonly<User> = { 
  name: "角田", 
  age: 28,
  email: "readonly@example.com"
};

// user5.name = "変更"; // ❌ コンパイルエラー: 読み取り専用

Record<K, T> - キーと値の型を指定

const userStatus: Record<"active" | "inactive" | "pending", number> = {
  active: 150,
  inactive: 20,
  pending: 5
};

const person: Record<"name" | "age", string | number> = {
  name: "Kakuta",
  age: 28
};

// 動的なキーを持つオブジェクトの型定義に便利
const apiEndpoints: Record<string, string> = {
  users: "/api/users",
  posts: "/api/posts",
  comments: "/api/comments"
};

Pick<T, K> - 指定したプロパティのみを抽出

type UserSummary = Pick<User, "name" | "email">;

const summary: UserSummary = {
  name: "Kakuta",
  email: "kakuta@example.com"
  // ageは含まれない
};

Omit<T, K> - 指定したプロパティを除外

type UserWithoutEmail = Omit<User, "email">;

const userNoEmail: UserWithoutEmail = {
  name: "Kakuta",
  age: 28
  // emailは除外される
};

それぞれのユーティリティ型の使い分け

  • Partial<T>: 更新処理、オプショナルな設定
  • Required<T>: 登録フォーム、必須チェック
  • Readonly<T>: 表示専用、設定値の保護
  • Pick<T, K>: API レスポンス、一覧表示
  • Omit<T, K>: センシティブ情報の除外
  • Record<K, T>: 設定値、マッピング情報

応用例:

// 基本のユーザー型
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  lastLoginDate: Date;
  isActive: boolean;
}

// 1. Partial<T> - ユーザー情報の部分更新
// 「全部じゃなくて、変更したい部分だけ渡したい」
class UserService {
  private users: User[] = [];

  // ユーザー更新:変更したいフィールドだけ渡せる
  updateUser(id: string, updates: Partial<User>): User | null {
    const userIndex = this.users.findIndex(u => u.id === id);
    if (userIndex === -1) return null;

    // 既存の情報に新しい情報を上書き
    this.users[userIndex] = { ...this.users[userIndex], ...updates };
    return this.users[userIndex];
  }

  // 使用例
  exampleUpdate(): void {
    // 名前だけ更新したい場合
    this.updateUser("user1", { name: "新しい名前" });

    // 年齢とアクティブ状態を更新したい場合
    this.updateUser("user2", {
      age: 25,
      isActive: false
    });
  }
}

// 2. Required<T> - ユーザー登録フォーム
// 「通常はオプショナルな項目も、登録時は必須にしたい」
interface UserProfile {
  name?: string;
  email?: string;
  age?: number;
  bio?: string;
}

// 登録時は全項目必須
type UserRegistrationForm = Required<UserProfile>;

function registerUser(formData: UserRegistrationForm): User {
  return {
    id: `user-${Date.now()}`,
    name: formData.name,        // 必須なので安心して使える
    email: formData.email,      // 必須なので安心して使える
    age: formData.age,          // 必須なので安心して使える
    lastLoginDate: new Date(),
    isActive: true
  };
}

// 3. Readonly<T> - 表示専用のユーザーデータ
// 「表示する際は、誤って変更されないようにしたい」
function displayUserInfo(user: Readonly<User>): string {
  // user.name = "変更"; // ❌ コンパイルエラー!変更できない

  return `
    名前: ${user.name}
    メール: ${user.email}
    年齢: ${user.age}歳
    最終ログイン: ${user.lastLoginDate.toDateString()}
    ステータス: ${user.isActive ? 'アクティブ' : '非アクティブ'}
  `;
}

// 4. Pick<T, K> - 必要な項目だけ抜き出し
// 「一覧表示では全項目は不要、必要な項目だけ表示したい」
type UserListItem = Pick<User, 'id' | 'name' | 'email' | 'isActive'>;

function getUserList(): UserListItem[] {
  const users: User[] = [
    {
      id: "1",
      name: "田中太郎",
      email: "tanaka@example.com",
      age: 30,
      lastLoginDate: new Date(),
      isActive: true
    }
  ];

  // 必要な項目だけを返す(軽量化)
  return users.map(user => ({
    id: user.id,
    name: user.name,
    email: user.email,
    isActive: user.isActive
  }));
}

// 5. Omit<T, K> - 特定の項目を除外
// 「パスワードなど、センシティブな情報は除外したい」
interface UserWithPassword {
  id: string;
  name: string;
  email: string;
  password: string;
  age: number;
}

// パスワードを除いた安全な型
type SafeUser = Omit<UserWithPassword, 'password'>;

function getUserForAPI(user: UserWithPassword): SafeUser {
  // パスワードを除外して返す
  const { password, ...safeUser } = user;
  return safeUser;
}

// 6. Record<K, T> - 動的なキーと値の組み合わせ
// 「設定値やマッピングを型安全に管理したい」
type UserSettings = Record<'theme' | 'language' | 'notifications', string>;

const defaultSettings: UserSettings = {
  theme: 'light',
  language: 'ja',
  notifications: 'enabled'
};

// エラーレベルマッピング
type ErrorLevel = 'info' | 'warning' | 'error';
type ErrorMessages = Record<ErrorLevel, string>;

const errorMessages: ErrorMessages = {
  info: '情報をお知らせします',
  warning: '注意が必要です',
  error: 'エラーが発生しました'
};

// 使用例の統合
function demonstrateUtilityTypes(): void {
  const userService = new UserService();

  // 部分更新
  userService.updateUser("user1", { name: "更新された名前" });

  // 登録フォーム(全項目必須)
  const registrationData: UserRegistrationForm = {
    name: "新規ユーザー",
    email: "newuser@example.com",
    age: 25,
    bio: "よろしくお願いします"
  };

  const newUser = registerUser(registrationData);

  // 読み取り専用で表示
  const displayText = displayUserInfo(newUser);
  console.log(displayText);

  // 一覧用の軽量データ
  const userList = getUserList();
  console.log(userList);
}

まとめ

TypeScriptのオブジェクト指向機能を活用することで、より安全で保守しやすいコードを書けます。

学んだポイント

  • クラスとインスタンス化: newキーワードでオブジェクトを生成
  • アクセス修飾子: publicprotectedprivateでアクセス範囲を制御
  • Getter/Setter: プロパティへの安全なアクセスを実現
  • readonly修飾子: 初期化後の変更を防止
  • staticメンバ: インスタンス作成不要の共通機能
  • 継承: 既存機能の再利用と拡張
  • インターフェース: オブジェクト構造の明確な定義
  • ユーティリティ型: 柔軟な型操作

最初はすべてを使いこなす必要はありません。まずは身近な問題から始めて、「あ、これはあの機能で解決できそう」と思ったときに導入してみてください。TypeScriptの真価は、バグを事前に防ぎ、開発効率を向上させることにあります。これらの機能を活用して、より安全で保守性の高いコードを書いていきましょう!

コメント

ログインしてコメントしましょう。