TypeScriptは、JavaScriptに強力な型システムを追加した言語として、大規模開発には欠かせない存在となりました。

中でもクラス(class)は、オブジェクト指向プログラミングを実現するための非常に重要な構成要素です。

2026年現在、TypeScriptのクラスはECMAScriptの標準進化と共に、より洗練された構文と高度な型安全性を備えています。

本記事では、TypeScriptにおけるクラスの基礎から、実践的なオブジェクト指向設計への応用までを詳しく紹介します。

初心者の方から、より堅牢なコードを書きたい中級者の方まで、幅広く役立つ内容となっています。

TypeScriptにおけるクラスの基本構造

TypeScriptのクラスは、JavaScriptのクラス構文をベースに、プロパティの型定義やアクセス制御を追加したものです。

まずは、最も基本的なクラスの定義方法から確認していきましょう。

TypeScript
// 基本的なクラスの定義
class User {
  // プロパティの宣言
  name: string;
  age: number;

  // コンストラクタ
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // メソッド
  greet(): string {
    return `こんにちは、私は${this.name}です。`;
  }
}

// インスタンスの生成
const user1 = new User("田中太郎", 25);
console.log(user1.greet());
実行結果
こんにちは、私は田中太郎です。

上記のコードでは、Userというクラスを定義し、名前と年齢を保持するプロパティを持たせています。

TypeScriptでは、コンストラクタ内で this を使用して代入する前に、クラスのトップレベルでプロパティとその型を宣言しておく必要があります

これにより、コンパイラはインスタンスがどのようなデータ構造を持つかを正確に把握できます。

コンストラクタと初期化の簡略化

TypeScriptには、プロパティの宣言とコンストラクタでの代入を一度に行うパラメータープロパティという便利な機能があります。

この機能を使用すると、コードの記述量を大幅に削減できます。

TypeScript
class SimpleUser {
  // アクセス修飾子を引数につけることで、プロパティ宣言と代入を同時に行う
  constructor(public name: string, public age: number) {}

  greet(): string {
    return `こんにちは、${this.name}(${this.age}歳)です。`;
  }
}

const user2 = new SimpleUser("佐藤花子", 30);
console.log(user2.greet());
実行結果
こんにちは、佐藤花子(30歳)です。

constructor(public name: string) のように記述するだけで、自動的にクラスのプロパティとして定義されます。

これはTypeScript特有の糖衣構文であり、実務でも非常によく使われるパターンです。

アクセス修飾子によるカプセル化

オブジェクト指向の重要な概念の一つに「カプセル化」があります。

TypeScriptでは、publicprivateprotected という3種類のアクセス修飾子を使用して、外部からのアクセスを制限できます。

public, private, protectedの使い分け

それぞれの修飾子の役割を正しく理解することは、安全なコード設計の第一歩です。

修飾子クラス内部子クラス内部インスタンス外部
public○ アクセス可能○ アクセス可能○ アクセス可能
protected○ アクセス可能○ アクセス可能× アクセス不可
private○ アクセス可能× アクセス不可× アクセス不可

デフォルトではすべてのプロパティとメソッドは public に設定されています。

内部状態を保護したい場合は、積極的に private を使用すべきです

TypeScript
class BankAccount {
  // 外部から直接書き換えられないようにprivateにする
  private balance: number;

  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }

  // 預け入れメソッド(公開インターフェース)
  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
      console.log(`${amount}円入金しました。`);
    }
  }

  // 残高確認メソッド
  public getBalance(): number {
    return this.balance;
  }
}

const myAccount = new BankAccount(1000);
myAccount.deposit(500);
console.log(`現在の残高: ${myAccount.getBalance()}円`);
// console.log(myAccount.balance); // エラー:'balance' はプライベートです。
実行結果
500円入金しました。
現在の残高: 1500円

このように、直接数値を操作させずにメソッド経由で操作させることで、不整合なデータ状態を防ぐことができます。

readonly修飾子

プロパティの値を初期化後に変更させたくない場合は、readonly 修飾子を使用します。

これは読み取り専用のプロパティを作成するためのもので、イミュータブル(不変)な設計に役立ちます。

TypeScript
class Config {
  readonly apiKey: string;

  constructor(key: string) {
    this.apiKey = key;
  }
}

const myConfig = new Config("secret_key_123");
// myConfig.apiKey = "new_key"; // エラー:読み取り専用プロパティのため代入できません。

ECMAScript標準のプライベートフィールド (#)

TypeScript 3.8以降、JavaScript(ES2020)標準のプライベートフィールドである # もサポートされています。

private 修飾子と # 記法には、実行時の挙動に大きな違いがあります。

TypeScript
class SecureData {
  #secretCode: string;

  constructor(code: string) {
    this.#secretCode = code;
  }

  showSecret() {
    console.log(this.#secretCode);
  }
}

private 修飾子はTypeScriptのコンパイル時のみチェックされますが、# 記法はJavaScriptの実行レベルでハードに隠蔽されます。

2026年現在のモダンな開発では、より厳格な隠蔽が必要な場合に # が好んで使われることも増えています。

クラスの継承とポリモーフィズム

クラスの強力な機能の一つが「継承(Inheritance)」です。

既存のクラスをベースにして、新しい機能を追加したり、振る舞いを上書きしたりできます。

extendsによる拡張

TypeScriptでは extends キーワードを使用して、他のクラスを継承します。

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

  makeSound(): void {
    console.log("音を出します。");
  }
}

// Animalを継承したDogクラス
class Dog extends Animal {
  constructor(name: string, public breed: string) {
    // 親クラスのコンストラクタを呼び出す
    super(name);
  }

  // メソッドのオーバーライド
  makeSound(): void {
    console.log("ワンワン!");
  }

  wagTail(): void {
    console.log("しっぽを振っています。");
  }
}

const myDog = new Dog("ポチ", "柴犬");
myDog.makeSound();
myDog.wagTail();
実行結果
ワンワン!
しっぽを振っています。

子クラスのコンストラクタでは、this を使用する前に必ず super() を呼び出して親クラスを初期化しなければなりません。

また、親クラスと同じ名前のメソッドを定義することで、振る舞いを上書きするオーバーライドが可能です。

抽象クラス(abstract class)の活用

「このクラスは直接インスタンス化してほしくないが、共通の構造は強制したい」という場合に、抽象クラスが役立ちます。

抽象クラスは abstract キーワードを使い、具体的な実装を持たない「抽象メソッド」を定義できます。

TypeScript
abstract class Shape {
  // 子クラスで必ず実装しなければならない抽象メソッド
  abstract getArea(): number;

  printArea(): void {
    console.log(`面積は ${this.getArea()} です。`);
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  getArea(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Square extends Shape {
  constructor(private width: number) {
    super();
  }

  getArea(): number {
    return this.width * this.width;
  }
}

const circle = new Circle(10);
circle.printArea();

const square = new Square(5);
square.printArea();
実行結果
面積は 314.1592653589793 です。
面積は 25 です。

抽象クラスを利用することで、設計のテンプレートを作成し、実装漏れを防ぐことができます。

これは特に複数人での開発や、大規模なシステムのアーキテクチャ設計において非常に有効です。

クラスとインターフェースの使い分け

TypeScriptには class の他に interface も存在します。

どちらも型を定義できますが、その目的は異なります。

  • class: 実装(メソッドの中身)や状態(プロパティの値)を持つ。インスタンスを作ることができる。
  • interface: 形(契約)だけを定義する。実行時のコードには残らない。

クラスに特定のインターフェースを遵守させたい場合は、implements を使用します。

TypeScript
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
}

このように、機能の「契約」をインターフェースで定義し、具体的な「実装」をクラスで記述するのが一般的な設計パターンです。

実践的なオブジェクト指向設計の始め方

クラスを使いこなすためには、単なる文法知識だけでなく、「何をクラスにするか」という設計の視点が必要です。

2026年のWeb開発においても、クリーンアーキテクチャやドメイン駆動設計(DDD)の考え方は重要視されています。

単一責任の原則(SRP)

一つのクラスには、一つの責任だけを持たせるようにしましょう。

例えば、「ユーザー情報の保持」と「データベースへの保存」を一つのクラスで行うのではなく、それぞれ別のクラスに分割します。

TypeScript
// データ保持のみを担当する(Entity)
class UserEntity {
  constructor(public readonly id: string, public name: string) {}
}

// 保存処理のみを担当する(Repository)
class UserRepository {
  save(user: UserEntity) {
    // データベースへの保存ロジック
    console.log(`${user.name}を保存しました。`);
  }
}

このように責務を分離することで、テストが容易になり、変更に強いコードになります。

依存性の注入(DI)の基礎

クラスが他のクラスに依存する場合、コンストラクタを介して外部からインスタンスを渡すように設計すると柔軟性が高まります。

TypeScript
class UserService {
  // UserRepositoryに依存しているが、外部から注入される
  constructor(private repo: UserRepository) {}

  createUser(name: string) {
    const newUser = new UserEntity(Date.now().toString(), name);
    this.repo.save(newUser);
  }
}

const repo = new UserRepository();
const service = new UserService(repo);
service.createUser("後藤");

この手法は依存性の注入(Dependency Injection)と呼ばれ、クラス間の結合度を下げるために極めて重要です。

ゲッターとセッター(Getter / Setter)

プロパティへのアクセスを制御するもう一つの手段として、get および set キーワードがあります。

これらを使うと、プロパティのように見せかけつつ、裏側でロジックを実行できます。

TypeScript
class Employee {
  private _salary: number = 0;

  get salary(): number {
    return this._salary;
  }

  set salary(value: number) {
    if (value < 0) {
      console.error("給与がマイナスになることはありません。");
      return;
    }
    this._salary = value;
  }
}

const emp = new Employee();
emp.salary = 500000; // setterが呼ばれる
console.log(emp.salary); // getterが呼ばれる

単なる値の代入ではなく、バリデーション(妥当性チェック)を挟みたい場合に非常に便利です。

まとめ

TypeScriptのクラスは、単なるJavaScriptの拡張にとどまらず、安全で再利用性の高いコードを書くための強力なツールです。

publicprivate によるアクセス制御を適切に行い、継承や抽象クラスを使いこなすことで、複雑なビジネスロジックも整理された形で実装できます。

また、インターフェースとの組み合わせや、依存性の注入といった設計パターンを取り入れることで、変化に強いアプリケーションを構築できるようになります。

まずは、小さなデータ構造をクラスで定義することから始めて、徐々にオブジェクト指向の恩恵を実感してみてください。

2026年の開発現場においても、これらの基本原則は変わらず、あなたのエンジニアとしての武器になるはずです。

この記事が、皆さんのTypeScriptスキルの向上に少しでも貢献できれば幸いです。