【TypeScript】クラスの型定義 完全ガイド|アクセス修飾子・抽象クラス・インターフェース実装まで

【TypeScript】クラスの型定義 完全ガイド|アクセス修飾子・抽象クラス・インターフェース実装まで TypeScript

TypeScriptでクラスを書くとき、JavaScriptのクラスとは異なり、アクセス修飾子抽象クラスインターフェース実装ジェネリクスなど、型安全なオブジェクト指向プログラミング(OOP)を実現するための豊富な機能が用意されています。

この記事では、TypeScriptのクラスに関する型定義を基本から実務レベルまで体系的に解説します。「private# の違いは?」「abstract classinterface のどちらを使うべき?」「ジェネリクスクラスの書き方がわからない」「デザインパターンをTypeScriptで型安全に実装したい」といった疑問をすべて解消できる内容です。

この記事で学べること

  • TypeScriptのクラスとJavaScriptクラスの違い
  • プロパティ・メソッドへの型注釈の基本
  • アクセス修飾子(public / private / protected / readonly)の使い分け
  • ECMAScript #プライベートフィールドと private 修飾子の違い
  • コンストラクタとパラメータプロパティ(constructor shorthand)
  • getter / setter の型定義とバリデーション活用
  • static プロパティ・メソッドの型とシングルトンパターン
  • interface の implements(単一・複数実装)
  • クラスの継承(extends)と型の関係
  • 抽象クラス(abstract class)の定義と活用
  • ジェネリクスクラス(Generic Classes)でコレクションやリポジトリを実装
  • クラスと型の互換性(構造的部分型 / Structural Typing)
  • Mixin パターンの型安全な実装
  • TC39 Stage 3 デコレータの型定義
  • 実務デザインパターン集(Singleton / Factory / Repository / Builder / Observer)
  • クラス vs 関数型アプローチの使い分け
  • よくあるエラーと対処法

前提知識:この記事はTypeScriptの基本型と関数の型定義を理解している方を対象としています。基本型から学びたい方は【TypeScript】型の書き方 完全入門を、関数の型定義を学びたい方は【TypeScript】関数の型定義 完全ガイドを先にお読みください。

スポンサーリンク
  1. TypeScriptのクラスとは ― JavaScriptクラスとの違い
  2. 基本のクラス定義と型注釈
    1. プロパティの型注釈
    2. プロパティの初期値とオプショナルプロパティ
    3. メソッドの型注釈
  3. アクセス修飾子(public / private / protected / readonly)
    1. public(デフォルト)
    2. private
    3. protected
    4. readonly
  4. #プライベートフィールド vs private 修飾子
    1. 実務での使い分けガイドライン
  5. コンストラクタとパラメータプロパティ(constructor shorthand)
    1. 通常の書き方 vs パラメータプロパティ
    2. パラメータプロパティと通常引数の混在
  6. getter / setter の型定義
    1. 実務パターン: バリデーション付きセッター
  7. static プロパティ・メソッドの型
    1. static ファクトリメソッド
  8. interface の implements(単一・複数実装)
    1. 単一インターフェースの実装
    2. 複数インターフェースの実装
  9. クラスの継承(extends)と型の関係
    1. override キーワード(TypeScript 4.3+)
  10. 抽象クラス(abstract class)
  11. ジェネリクスクラス(Generic Classes)
    1. 基本のジェネリクスクラス
    2. 型制約付きジェネリクス
    3. 複数の型パラメータ
  12. クラスと型の互換性(構造的部分型)
  13. Mixin パターンの型定義
  14. デコレータ(TC39 Stage 3)の型定義
    1. メソッドデコレータ
    2. クラスデコレータ
  15. 実務デザインパターン集
    1. Singleton パターン
    2. Builder パターン
    3. Observer パターン
  16. クラス vs 関数型 ― どちらを選ぶべきか
  17. よくあるエラーと対処法
    1. Property has no initializer の対処
    2. this が undefined になる問題
  18. まとめ

TypeScriptのクラスとは ― JavaScriptクラスとの違い

ES2015(ES6)で導入されたJavaScriptのクラスは、プロトタイプベースの継承をクラス構文でわかりやすく記述できるようにしたシンタックスシュガーです。TypeScriptはこのJavaScriptクラスの構文をすべてサポートした上で、型注釈アクセス修飾子抽象クラスインターフェース実装といった型安全な機能を追加しています。

機能 JavaScript TypeScript
プロパティ型注釈 なし(すべて動的) name: string のように型を指定
アクセス修飾子 #(Private Fields)のみ public / private / protected / readonly
パラメータプロパティ なし constructor(public name: string)
抽象クラス なし abstract class
インターフェース実装 なし class Foo implements IFoo
ジェネリクス なし class Box<T>
コンパイル結果 そのまま実行 JSに変換(型情報は消える)

注意:TypeScriptのアクセス修飾子(privateprotected)やインターフェースなどの型情報は、コンパイル後のJavaScriptには存在しません。これらはコンパイル時のチェック専用であり、実行時には一切の制約を持ちません。実行時のプライベート性を保証するにはECMAScript の # プライベートフィールドを使います。

まずは、JavaScriptのクラスとTypeScriptのクラスの基本的なコードの違いを確認しましょう。

JavaScript のクラス
class User {
  constructor(name, age) {
    this.name = name;  // 型チェックなし
    this.age = age;    // どんな値でも代入可能
  }

  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

const user = new User(42, 'twenty'); // エラーにならない!
TypeScript のクラス
class User {
  name: string;   // プロパティの型を宣言
  age: number;    // 型注釈が必須

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

  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

const user = new User(42, 'twenty'); // コンパイルエラー!
// Argument of type 'number' is not assignable to parameter of type 'string'

TypeScriptではプロパティに型注釈を付け、コンストラクタの引数にも型を指定することで、不正な値の代入をコンパイル時に検出できます。これがTypeScriptのクラスにおける型安全性の基本です。

基本のクラス定義と型注釈

TypeScriptのクラスでは、プロパティとメソッドに型注釈を付けることで、型安全なオブジェクトを構築できます。ここでは基本的なクラス定義の書き方を確認していきましょう。

プロパティの型注釈

TypeScriptでは、クラスのプロパティをコンストラクタの外で事前に宣言し、型を指定する必要があります。JavaScriptでは this.name = name とコンストラクタ内で直接代入できますが、TypeScriptでは先にフィールドを宣言しないとエラーになります。

プロパティの型注釈
class Product {
  // プロパティの型を宣言
  name: string;
  price: number;
  inStock: boolean;
  tags: string[];

  constructor(
    name: string,
    price: number,
    inStock: boolean,
    tags: string[]
  ) {
    this.name = name;
    this.price = price;
    this.inStock = inStock;
    this.tags = tags;
  }
}

const product = new Product(
  'TypeScript Book',
  3980,
  true,
  ['typescript', 'programming']
);

プロパティの初期値とオプショナルプロパティ

プロパティに初期値を設定すると、コンストラクタでの代入を省略できます。また、? を付けてオプショナルプロパティにすることも可能です。

初期値とオプショナルプロパティ
class Config {
  // 初期値あり(型は推論される)
  debug = false;         // boolean と推論
  maxRetries = 3;       // number と推論
  baseUrl = 'https://api.example.com';

  // オプショナルプロパティ(undefined の可能性あり)
  timeout?: number;      // number | undefined
  apiKey?: string;       // string | undefined
}

const config = new Config();
console.log(config.debug);      // false
console.log(config.timeout);    // undefined

ポイント:初期値を設定した場合、TypeScriptは型を自動推論するため、明示的な型注釈は不要です。ただし、debug: boolean = false のように明示しても問題ありません。チームの規約に合わせましょう。

メソッドの型注釈

クラスのメソッドは、通常の関数と同じように引数の型戻り値の型を指定できます。

メソッドの型注釈
class Calculator {
  private result: number = 0;

  add(value: number): this {
    this.result += value;
    return this;
  }

  subtract(value: number): this {
    this.result -= value;
    return this;
  }

  multiply(value: number): this {
    this.result *= value;
    return this;
  }

  getResult(): number {
    return this.result;
  }

  reset(): void {
    this.result = 0;
  }
}

const calc = new Calculator();
const answer = calc.add(10).multiply(3).subtract(5).getResult();
console.log(answer); // 25

実行結果

25

アクセス修飾子(public / private / protected / readonly)

TypeScriptのアクセス修飾子は、クラスのプロパティやメソッドのアクセス範囲を制御するための仕組みです。JavaScriptにはない(# を除く)TypeScript独自の機能であり、カプセル化を実現する上で非常に重要です。

修飾子 クラス内 サブクラス クラス外 用途
public OK OK OK どこからでもアクセス可能(デフォルト)
private OK NG NG クラス内部でのみアクセス
protected OK OK NG クラスとサブクラスからアクセス
readonly 初期化時のみ 読取のみ 読取のみ 一度設定したら変更不可

public(デフォルト)

public はどこからでもアクセスできる修飾子です。TypeScriptでは何も指定しない場合のデフォルトが public なので、明示的に書く必要はありませんが、チームの規約で明示することもあります。

public 修飾子
class Animal {
  public name: string;    // 明示的に public
  species: string;        // 暗黙的に public(同じ意味)

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

const dog = new Animal('Buddy', 'Dog');
console.log(dog.name);      // OK: 'Buddy'
console.log(dog.species);   // OK: 'Dog'

private

private 修飾子は、そのプロパティやメソッドをクラス内部からのみアクセスできるようにします。サブクラスや外部からはアクセスできません。

private 修飾子
class BankAccount {
  private balance: number;
  private owner: string;

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

  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Amount must be positive');
    this.balance += amount;
  }

  getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount('Alice', 1000);
account.deposit(500);
console.log(account.getBalance()); // 1500

// account.balance;  // エラー: Property 'balance' is private

protected

protected 修飾子は、クラス内部とサブクラスからアクセスできるが、外部からはアクセスできない修飾子です。継承階層でデータを共有したいが、外部には公開したくない場合に使います。

protected 修飾子
class BaseLogger {
  protected prefix: string;

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

  protected formatMessage(msg: string): string {
    return `[${this.prefix}] ${msg}`;
  }

  log(msg: string): void {
    console.log(this.formatMessage(msg));
  }
}

class TimestampLogger extends BaseLogger {
  log(msg: string): void {
    // protected メンバーにサブクラスからアクセスできる
    const formatted = this.formatMessage(msg);
    console.log(`${new Date().toISOString()} ${formatted}`);
  }
}

const logger = new TimestampLogger('APP');
logger.log('Server started'); // OK

// logger.prefix;             // エラー: Property 'prefix' is protected
// logger.formatMessage('x'); // エラー: Property 'formatMessage' is protected

readonly

readonly 修飾子は、プロパティを読み取り専用にします。初期化時(宣言時またはコンストラクタ内)にのみ値を設定でき、以降は変更できません。他の修飾子と組み合わせることもできます。

readonly 修飾子
class DatabaseConfig {
  readonly host: string;
  readonly port: number;
  private readonly password: string;

  constructor(host: string, port: number, pw: string) {
    this.host = host;
    this.port = port;
    this.password = pw;
  }

  getConnectionString(): string {
    return `postgresql://${this.host}:${this.port}`;
  }
}

const db = new DatabaseConfig('localhost', 5432, 'secret');
console.log(db.host); // OK: 'localhost'

// db.host = 'other';  // エラー: Cannot assign to 'host' because it is a read-only property

ポイント:readonly はオブジェクトの参照先の変更を防ぐだけで、参照先オブジェクトのプロパティ変更は防ぎません。配列なら readonly string[]ReadonlyArray<string> を使いましょう。

#プライベートフィールド vs private 修飾子

TypeScriptには、クラスメンバーを非公開にする方法が2つあります。TypeScriptの private 修飾子と、ECMAScript標準の #(ハッシュプレフィックス)プライベートフィールドです。両者は似ているようで、動作が大きく異なります。

比較項目 private 修飾子 # プライベートフィールド
チェックタイミング コンパイル時のみ コンパイル時 + 実行時
JSコンパイル後 通常のプロパティとして残る #付きのまま残る(真のプライベート)
実行時アクセス (obj as any).field でアクセス可能 絶対にアクセス不可
サブクラスでの同名 エラー 独立したフィールドとして共存可能
構造的型互換性 private が型チェックに影響 外部から見えないので影響しない
推奨シーン 一般的な非公開メンバー セキュリティが重要な場合
private 修飾子 vs # プライベートフィールド
// ── TypeScript の private ──
class UserA {
  private secret = 'ts-private';

  getSecret(): string {
    return this.secret;
  }
}

const a = new UserA();
// a.secret;                // コンパイルエラー
console.log((a as any).secret); // 'ts-private' ← 実行時はアクセス可能!

// ── ECMAScript の # プライベートフィールド ──
class UserB {
  #secret = 'es-private';

  getSecret(): string {
    return this.#secret;
  }
}

const b = new UserB();
// b.#secret;               // 構文エラー
// (b as any).#secret;      // 構文エラー(回避不可能)
console.log(b.getSecret()); // 'es-private'

注意:private 修飾子はコンパイル後のJavaScriptでは消えるため、実行時には (obj as any).field でアクセスできてしまいます。セキュリティやライブラリの内部実装で真のプライベート性が必要な場合は # を使いましょう。

実務での使い分けガイドライン

一般的なアプリケーション開発では private 修飾子で十分です。# プライベートフィールドは以下のケースで検討しましょう。

# プライベートフィールドを使うべきケース

  • ライブラリ・フレームワーク開発: 利用者に内部実装を触らせたくない
  • セキュリティ重視: トークンやパスワードなど、絶対に外部公開したくないデータ
  • サブクラスとの名前衝突回避: 同名のフィールドを親子で独立管理したい

コンストラクタとパラメータプロパティ(constructor shorthand)

TypeScriptのパラメータプロパティは、コンストラクタの引数にアクセス修飾子を付けることで、プロパティの宣言と初期化を1行で行える便利な省略記法です。

通常の書き方 vs パラメータプロパティ

通常の書き方(冗長)
class User {
  private name: string;
  private age: number;
  readonly id: string;

  constructor(name: string, age: number, id: string) {
    this.name = name;   // 3回も同じことを書いている
    this.age = age;
    this.id = id;
  }
}
パラメータプロパティ(簡潔)
class User {
  constructor(
    private name: string,
    private age: number,
    readonly id: string
  ) {}
  // ↑ プロパティ宣言 + 初期化が自動で行われる

  getName(): string {
    return this.name;  // this.name は private string として使える
  }
}

const user = new User('Alice', 30, 'user-001');
console.log(user.id);        // OK: 'user-001'(readonly public)
console.log(user.getName()); // OK: 'Alice'
// user.name;   // エラー: Property 'name' is private
// user.id = 'new'; // エラー: Cannot assign to 'id' because it is a read-only property

ポイント:パラメータプロパティが有効になるのは、引数に publicprivateprotectedreadonly のいずれかが付いている場合です。修飾子なしの引数は通常の引数として扱われ、自動でプロパティにはなりません。

パラメータプロパティと通常引数の混在

パラメータプロパティと通常の引数を混在させることもできます。修飾子付きの引数は自動でプロパティになり、修飾子なしの引数はコンストラクタ内でのみ使えます。

混在パターン
class ApiClient {
  private token: string;

  constructor(
    public readonly baseUrl: string,  // パラメータプロパティ
    private readonly apiKey: string, // パラメータプロパティ
    secretSalt: string              // 通常の引数(プロパティにならない)
  ) {
    // secretSalt はコンストラクタ内でのみ使える
    this.token = `${apiKey}-${secretSalt}`;
  }

  getToken(): string {
    return this.token;
  }
}

const client = new ApiClient('https://api.example.com', 'key123', 'salt456');
console.log(client.baseUrl);    // OK: 'https://api.example.com'
console.log(client.getToken()); // OK: 'key123-salt456'

getter / setter の型定義

TypeScriptの get / set アクセサを使うと、プロパティへのアクセスにロジック(バリデーション、変換、遅延計算など)を挿入できます。外部からはプロパティに見えますが、内部では関数が実行されます。

getter / setter の基本
class Temperature {
  private _celsius: number;

  constructor(celsius: number) {
    this._celsius = celsius;
  }

  // getter: 戻り値の型を指定
  get celsius(): number {
    return this._celsius;
  }

  // setter: 引数の型を指定(戻り値は常に void)
  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error('Temperature below absolute zero');
    }
    this._celsius = value;
  }

  // 計算プロパティ(getter のみ = 読み取り専用)
  get fahrenheit(): number {
    return this._celsius * 1.8 + 32;
  }

  get kelvin(): number {
    return this._celsius + 273.15;
  }
}

const temp = new Temperature(100);
console.log(temp.celsius);    // 100
console.log(temp.fahrenheit);  // 212
console.log(temp.kelvin);      // 373.15

temp.celsius = 0; // OK
// temp.fahrenheit = 100; // エラー: getter のみなので代入不可

実行結果

100
212
373.15

ポイント:TypeScript 4.3以降では、getter と setter で異なる型を持たせることが可能です。例えば setter で string | number を受け取り、getter で number のみを返す、といった設計ができます。

実務パターン: バリデーション付きセッター

バリデーション付きクラス
class UserProfile {
  private _email = '';
  private _age = 0;

  get email(): string {
    return this._email;
  }

  set email(value: string) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      throw new Error(`Invalid email: ${value}`);
    }
    this._email = value;
  }

  get age(): number {
    return this._age;
  }

  set age(value: number) {
    if (value < 0 || value > 150) {
      throw new RangeError(`Age must be 0-150, got ${value}`);
    }
    this._age = Math.floor(value);
  }
}

static プロパティ・メソッドの型

static メンバーは、インスタンスではなくクラス自体に属するプロパティやメソッドです。new しなくても ClassName.method() の形で呼び出せます。ユーティリティ関数やファクトリメソッド、定数の定義に使います。

static メンバーの基本
class MathUtils {
  // static readonly で定数を定義
  static readonly PI = 3.14159265358979;
  static readonly E = 2.71828182845905;

  // static メソッド
  static clamp(value: number, min: number, max: number): number {
    return Math.max(min, Math.min(max, value));
  }

  static lerp(a: number, b: number, t: number): number {
    return a + (b - a) * t;
  }

  // private constructor でインスタンス化を禁止
  private constructor() {}
}

console.log(MathUtils.PI);            // 3.14159265358979
console.log(MathUtils.clamp(15, 0, 10)); // 10
console.log(MathUtils.lerp(0, 100, 0.5)); // 50

// const m = new MathUtils(); // エラー: Constructor is private

static ファクトリメソッド

コンストラクタの代わりに static メソッドでインスタンスを生成するパターンです。名前付きコンストラクタ(Named Constructor)とも呼ばれ、複数の生成方法を提供したい場合に便利です。

static ファクトリメソッド
class Color {
  private constructor(
    public readonly r: number,
    public readonly g: number,
    public readonly b: number
  ) {}

  static fromRGB(r: number, g: number, b: number): Color {
    return new Color(r, g, b);
  }

  static fromHex(hex: string): Color {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    if (!result) throw new Error(`Invalid hex: ${hex}`);
    return new Color(
      parseInt(result[1], 16),
      parseInt(result[2], 16),
      parseInt(result[3], 16)
    );
  }

  // よく使う色を定数として定義
  static readonly RED = Color.fromRGB(255, 0, 0);
  static readonly GREEN = Color.fromRGB(0, 255, 0);
  static readonly BLUE = Color.fromRGB(0, 0, 255);

  toString(): string {
    return `rgb(${this.r}, ${this.g}, ${this.b})`;
  }
}

const coral = Color.fromHex('#FF7F50');
console.log(coral.toString()); // rgb(255, 127, 80)
console.log(Color.RED.toString()); // rgb(255, 0, 0)

interface の implements(単一・複数実装)

TypeScriptでは、interface で「クラスが持つべきメンバーの契約」を定義し、implements キーワードでクラスにその契約を実装させます。これにより、クラスが必要なメソッドやプロパティを持っていることをコンパイル時に保証できます。

単一インターフェースの実装

implements の基本
// インターフェースで契約を定義
interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

interface Printable {
  print(): void;
}

// クラスでインターフェースを実装
class Document implements Serializable {
  constructor(
    public title: string,
    public content: string
  ) {}

  serialize(): string {
    return JSON.stringify({ title: this.title, content: this.content });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.title = parsed.title;
    this.content = parsed.content;
  }
}

// インターフェース型として扱える
const doc: Serializable = new Document('Hello', 'World');
console.log(doc.serialize()); // {"title":"Hello","content":"World"}

複数インターフェースの実装

TypeScriptのクラスは複数のインターフェースを同時に実装できます。JavaやC#と同様の「多重インターフェース実装」パターンです。

複数インターフェースの実装
interface Loggable {
  log(message: string): void;
}

interface Disposable {
  dispose(): void;
  isDisposed: boolean;
}

interface Configurable {
  configure(options: Record<string, unknown>): void;
}

// 3つのインターフェースを同時に実装
class Service implements Loggable, Disposable, Configurable {
  isDisposed = false;

  log(message: string): void {
    console.log(`[Service] ${message}`);
  }

  dispose(): void {
    this.isDisposed = true;
    this.log('Disposed');
  }

  configure(options: Record<string, unknown>): void {
    this.log(`Configured with ${JSON.stringify(options)}`);
  }
}

注意:implements はコンパイル時の型チェックのみを行います。インターフェースのメソッドの実装コードが自動生成されるわけではありません。すべてのメンバーを自分で実装する必要があります。

クラスの継承(extends)と型の関係

TypeScriptのクラスは extends キーワードで単一継承をサポートします。サブクラスは親クラスのすべての public / protected メンバーを引き継ぎ、さらに独自のメンバーを追加できます。

クラスの継承
class Shape {
  constructor(
    public color: string,
    protected x: number,
    protected y: number
  ) {}

  move(dx: number, dy: number): void {
    this.x += dx;
    this.y += dy;
  }

  describe(): string {
    return `${this.color} shape at (${this.x}, ${this.y})`;
  }
}

class Circle extends Shape {
  constructor(
    color: string,
    x: number,
    y: number,
    public radius: number
  ) {
    super(color, x, y); // 親のコンストラクタを呼ぶ
  }

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

  // メソッドのオーバーライド
  describe(): string {
    return `${this.color} circle (r=${this.radius}) at (${this.x}, ${this.y})`;
  }
}

class Rectangle extends Shape {
  constructor(
    color: string,
    x: number,
    y: number,
    public width: number,
    public height: number
  ) {
    super(color, x, y);
  }

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

// 多態性(ポリモーフィズム): 親クラスの型で子クラスを扱える
const shapes: Shape[] = [
  new Circle('red', 0, 0, 5),
  new Rectangle('blue', 10, 10, 20, 30)
];

shapes.forEach(s => console.log(s.describe()));

実行結果

red circle (r=5) at (0, 0)
blue shape at (10, 10)

override キーワード(TypeScript 4.3+)

TypeScript 4.3以降では、override キーワードを使って「このメソッドは親クラスのメソッドをオーバーライドしている」ことを明示できます。tsconfig.json"noImplicitOverride": true を設定すると、override の付け忘れがエラーになります。

override キーワード
class Animal {
  speak(): string {
    return '...';
  }
}

class Dog extends Animal {
  override speak(): string {  // 明示的にオーバーライドを宣言
    return 'Woof!';
  }

  // override swim(): string {  // エラー: 親に swim() がない
  //   return 'splash';
  // }
}

ポイント:override キーワードを使うと、親クラスのメソッド名を変更した際にサブクラスのオーバーライドが壊れていることをコンパイルエラーで検出できます。大規模プロジェクトでは "noImplicitOverride": true の設定を推奨します。

抽象クラス(abstract class)

抽象クラスは、直接インスタンス化できないクラスです。サブクラスに「必ず実装すべきメソッド」を強制しつつ、共通の実装も提供できます。interfaceclass の中間的な存在として、テンプレートメソッドパターンなどで活用されます。

特徴 interface abstract class class
インスタンス化 不可 不可 可能
実装コード 持てない 持てる 持てる
未実装メソッド すべて未実装 abstract 付きのみ未実装 不可
アクセス修飾子 なし あり あり
多重継承/実装 複数 implements 可 単一 extends のみ 単一 extends のみ
抽象クラスの定義と実装
abstract class PaymentProcessor {
  constructor(protected merchantId: string) {}

  // 抽象メソッド: サブクラスで必ず実装する
  abstract charge(amount: number, currency: string): Promise<{ success: boolean; transactionId: string }>;
  abstract refund(transactionId: string): Promise<boolean>;

  // 具象メソッド: 共通ロジックを提供
  validateAmount(amount: number): boolean {
    return amount > 0 && amount <= 999999;
  }

  // テンプレートメソッドパターン
  async processPayment(amount: number, currency: string) {
    if (!this.validateAmount(amount)) {
      throw new Error('Invalid amount');
    }
    console.log(`Processing ${amount} ${currency}...`);
    return this.charge(amount, currency);
  }
}

// 具象クラス: Stripe用の実装
class StripeProcessor extends PaymentProcessor {
  async charge(amount: number, currency: string) {
    // Stripe API を呼び出す実装
    return { success: true, transactionId: `stripe_${Date.now()}` };
  }

  async refund(transactionId: string) {
    console.log(`Refunding ${transactionId} via Stripe`);
    return true;
  }
}

// const p = new PaymentProcessor('m1');  // エラー: 抽象クラスはインスタンス化できない
const stripe = new StripeProcessor('merchant_123');
stripe.processPayment(5000, 'JPY');

abstract class を使うべきケース

  • 共通のロジックをサブクラスに提供しつつ、一部を各サブクラスに実装させたい
  • テンプレートメソッドパターン: 処理の流れ(テンプレート)を親で定義し、具体的なステップをサブクラスで実装
  • protected メンバーが必要な場合(interface では定義できない)
  • コンストラクタロジックを共有したい場合

ジェネリクスクラス(Generic Classes)

ジェネリクスクラスは、型パラメータを持つクラスです。クラスの利用時に具体的な型を指定することで、1つのクラス定義で複数の型に対応できます。コレクション、リポジトリ、キャッシュなどの汎用的なデータ構造で頻繁に使われます。

基本のジェネリクスクラス

ジェネリクスクラスの基本
// 型パラメータ T を持つクラス
class Box<T> {
  private content: T;

  constructor(value: T) {
    this.content = value;
  }

  getValue(): T {
    return this.content;
  }

  setValue(value: T): void {
    this.content = value;
  }
}

// 使用時に型を指定
const numberBox = new Box<number>(42);
console.log(numberBox.getValue()); // 42 (型: number)

const stringBox = new Box<string>('hello');
console.log(stringBox.getValue()); // 'hello' (型: string)

// 型推論も可能
const inferredBox = new Box([1, 2, 3]); // Box<number[]> と推論

型制約付きジェネリクス

extends を使って型パラメータに制約を設けることで、特定のプロパティやメソッドを持つ型のみを受け入れるクラスを定義できます。

型制約付きジェネリクスクラス
// id プロパティを持つ型のみ受け入れる
interface HasId {
  id: string | number;
}

class Repository<T extends HasId> {
  private items: Map<string | number, T> = new Map();

  add(item: T): void {
    this.items.set(item.id, item); // T は HasId なので .id にアクセス可能
  }

  findById(id: string | number): T | undefined {
    return this.items.get(id);
  }

  getAll(): T[] {
    return [...this.items.values()];
  }

  delete(id: string | number): boolean {
    return this.items.delete(id);
  }
}

// 具体的な型で使う
interface User {
  id: string;
  name: string;
  email: string;
}

const userRepo = new Repository<User>();
userRepo.add({ id: 'u1', name: 'Alice', email: 'alice@example.com' });
const user = userRepo.findById('u1'); // 型: User | undefined

// HasId を満たさない型はエラー
// const repo = new Repository<string>(); // エラー: string は HasId を満たさない

複数の型パラメータ

複数の型パラメータ
class KeyValueStore<K, V> {
  private store = new Map<K, V>();

  set(key: K, value: V): void {
    this.store.set(key, value);
  }

  get(key: K): V | undefined {
    return this.store.get(key);
  }

  entries(): [K, V][] {
    return [...this.store.entries()];
  }
}

const cache = new KeyValueStore<string, number>();
cache.set('score', 100);
console.log(cache.get('score')); // 100

クラスと型の互換性(構造的部分型)

TypeScriptの型システムは構造的部分型(Structural Typing)を採用しています。これは、2つの型が同じ構造(プロパティとメソッド)を持っていれば、名前が異なっていても互換性があるとみなされる仕組みです。

Java や C# などの公称型(Nominal Typing)の言語では、クラス名が異なれば別の型ですが、TypeScriptでは構造が同じなら互換性があります。

構造的部分型の例
class Point2D {
  constructor(public x: number, public y: number) {}
}

class Coordinate {
  constructor(public x: number, public y: number) {}
}

// Point2D と Coordinate は同じ構造 → 互換性あり
const point: Point2D = new Coordinate(10, 20); // OK!

// オブジェクトリテラルでも OK
const p: Point2D = { x: 5, y: 10 }; // OK!

// より多くのプロパティを持つ型は、少ないプロパティの型に代入可能
class Point3D {
  constructor(public x: number, public y: number, public z: number) {}
}

const point2d: Point2D = new Point3D(1, 2, 3); // OK! (x, y があるので)

注意:private または protected メンバーを持つクラスは、同じ継承階層にある場合のみ型互換性があります。異なるクラスで同じ構造の private メンバーを持っていても互換性はありません。

private メンバーと型互換性
class Cat {
  private name: string;
  constructor(name: string) { this.name = name; }
}

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

// const cat: Cat = new Dog('Rex');
// エラー: private メンバーが異なる出自のため互換性がない

Mixin パターンの型定義

TypeScriptはクラスの単一継承のみをサポートしていますが、Mixin パターンを使うことで、複数のクラスの機能を1つのクラスに合成できます。

Mixin は「特定の機能を追加する関数」として実装します。クラスを受け取り、その機能を追加した新しいクラスを返す高階関数です。

Mixin パターンの型安全な実装
// コンストラクタ型を定義
type Constructor<T = {}> = new (...args: any[]) => T;

// Mixin 1: タイムスタンプ機能を追加
function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    createdAt = new Date();
    updatedAt = new Date();

    touch() {
      this.updatedAt = new Date();
    }
  };
}

// Mixin 2: ソフトデリート機能を追加
function SoftDeletable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isDeleted = false;
    deletedAt?: Date;

    softDelete() {
      this.isDeleted = true;
      this.deletedAt = new Date();
    }

    restore() {
      this.isDeleted = false;
      this.deletedAt = undefined;
    }
  };
}

// ベースクラス
class BaseEntity {
  constructor(public id: string) {}
}

// Mixin を合成
const EnhancedEntity = SoftDeletable(Timestamped(BaseEntity));

class Post extends EnhancedEntity {
  constructor(id: string, public title: string) {
    super(id);
  }
}

const post = new Post('p1', 'Hello World');
console.log(post.id);        // 'p1' (BaseEntity)
console.log(post.createdAt);  // Date (Timestamped)
console.log(post.isDeleted);  // false (SoftDeletable)
post.softDelete();
console.log(post.isDeleted);  // true

ポイント:Mixin パターンは TypeScript の公式ドキュメントでも推奨されている手法です。単一継承の制約を超えて機能を合成でき、interface の implements では実装コードを共有できない問題を解決します。

デコレータ(TC39 Stage 3)の型定義

TypeScript 5.0 以降では、TC39 Stage 3 の新しいデコレータ構文がサポートされています。デコレータは、クラスやそのメンバーに対してメタプログラミング(宣言的な機能追加)を行う仕組みです。

注意:TypeScript 5.0 の新デコレータは、tsconfig.json"experimentalDecorators": true を設定する旧デコレータとは異なります。新デコレータは特別な設定なしで使用でき、TC39標準に準拠しています。

メソッドデコレータ

メソッドデコレータの例
// ログ出力デコレータ
function log<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  const methodName = String(context.name);

  return function (this: This, ...args: Args): Return {
    console.log(`Calling ${methodName} with`, args);
    const result = target.call(this, ...args);
    console.log(`${methodName} returned`, result);
    return result;
  };
}

class MathService {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}

const math = new MathService();
math.add(2, 3);
// Calling add with [2, 3]
// add returned 5

クラスデコレータ

クラスデコレータ(Sealed パターン)
// クラスを凍結するデコレータ
function sealed(
  target: Function,
  _context: ClassDecoratorContext
) {
  Object.seal(target);
  Object.seal(target.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return `Hello, ${this.greeting}`;
  }
}

実務デザインパターン集

ここからは、TypeScriptのクラスを使った実務でよく使うデザインパターンを型安全に実装する方法を紹介します。

Singleton パターン

アプリケーション全体で1つだけのインスタンスを共有するパターンです。設定管理、データベース接続プール、ロガーなどに使います。

Singleton パターン
class AppConfig {
  private static instance: AppConfig;
  private settings: Map<string, unknown> = new Map();

  private constructor() {} // 外部からのインスタンス化を禁止

  static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig();
    }
    return AppConfig.instance;
  }

  get<T>(key: string): T | undefined {
    return this.settings.get(key) as T | undefined;
  }

  set(key: string, value: unknown): void {
    this.settings.set(key, value);
  }
}

const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();
console.log(config1 === config2); // true(同一インスタンス)

Builder パターン

複雑なオブジェクトをステップバイステップで構築するパターンです。コンストラクタの引数が多すぎる場合に特に有効です。

Builder パターン
interface HttpRequest {
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers: Record<string, string>;
  body?: string;
  timeout: number;
}

class RequestBuilder {
  private request: Partial<HttpRequest> = {};

  setUrl(url: string): this {
    this.request.url = url;
    return this;
  }

  setMethod(method: HttpRequest['method']): this {
    this.request.method = method;
    return this;
  }

  addHeader(key: string, value: string): this {
    this.request.headers = { ...this.request.headers, [key]: value };
    return this;
  }

  setBody(body: object): this {
    this.request.body = JSON.stringify(body);
    this.request.headers = {
      ...this.request.headers,
      'Content-Type': 'application/json'
    };
    return this;
  }

  setTimeout(ms: number): this {
    this.request.timeout = ms;
    return this;
  }

  build(): HttpRequest {
    if (!this.request.url) throw new Error('URL is required');
    return {
      url: this.request.url,
      method: this.request.method ?? 'GET',
      headers: this.request.headers ?? {},
      body: this.request.body,
      timeout: this.request.timeout ?? 5000,
    };
  }
}

const req = new RequestBuilder()
  .setUrl('https://api.example.com/users')
  .setMethod('POST')
  .addHeader('Authorization', 'Bearer token123')
  .setBody({ name: 'Alice' })
  .setTimeout(10000)
  .build();

Observer パターン

オブジェクトの状態変化を購読者に自動通知するパターンです。イベントシステムやリアクティブプログラミングの基礎になります。

Observer パターン(型安全なイベントエミッタ)
class EventEmitter<Events extends Record<string, any>> {
  private listeners = new Map<string, Set<Function>>();

  on<K extends keyof Events & string>(
    event: K,
    listener: (data: Events[K]) => void
  ): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
  }

  emit<K extends keyof Events & string>(
    event: K,
    data: Events[K]
  ): void {
    this.listeners.get(event)?.forEach(fn => fn(data));
  }
}

// イベントの型を定義
interface UserEvents {
  login: { userId: string; timestamp: Date };
  logout: { userId: string };
  error: { code: number; message: string };
}

const emitter = new EventEmitter<UserEvents>();

// 型安全: data は { userId: string; timestamp: Date }
emitter.on('login', (data) => {
  console.log(`User ${data.userId} logged in`);
});

emitter.emit('login', { userId: 'u1', timestamp: new Date() });
// emitter.emit('login', { userId: 123 }); // エラー: number は string に割り当てられない

クラス vs 関数型 ― どちらを選ぶべきか

TypeScriptでは、同じ機能をクラスベースでも関数型(クロージャ + 型)でも実装できます。どちらを選ぶべきかは、チームの慣習やユースケースによって異なります。

観点 クラスベース 関数型
状態管理 this でインスタンス状態を管理 クロージャで状態をカプセル化
継承 extends で階層的に拡張 関数合成(compose)で拡張
テスタビリティ DI(依存注入)でモック可能 引数で依存を渡す(純粋関数)
this の問題 コールバックで this を失う可能性 this を使わないので問題なし
tree shaking 未使用メソッドも含まれやすい 未使用関数は除外しやすい
適したケース 複雑な状態 + 継承が必要な場合 ステートレスなロジック、ユーティリティ
同じ機能のクラス版と関数版
// ── クラスベース ──
class Counter {
  private count = 0;

  increment(): number { return ++this.count; }
  decrement(): number { return --this.count; }
  getCount(): number { return this.count; }
}

// ── 関数型(クロージャ) ──
function createCounter() {
  let count = 0;

  return {
    increment: (): number => ++count,
    decrement: (): number => --count,
    getCount: (): number => count,
  };
}

// どちらも同じように使える
const c1 = new Counter();
c1.increment(); // 1

const c2 = createCounter();
c2.increment(); // 1

使い分けの指針

  • クラスを使う: 状態を持つオブジェクト、継承やポリモーフィズムが必要な場合、DI(依存注入)パターン
  • 関数型を使う: ステートレスなユーティリティ、純粋関数、React のカスタムフックなど
  • 実務では混在OK: 無理にどちらかに統一する必要はなく、適材適所で選ぶ

よくあるエラーと対処法

TypeScriptのクラスで遭遇しやすいエラーと、その対処法をまとめます。

エラー 原因 対処法
Property 'x' has no initializer strictPropertyInitialization が有効でプロパティ未初期化 コンストラクタで初期化するか !(definite assignment assertion)を使う
Property 'x' is private クラス外から private メンバーにアクセスしている public メソッド経由でアクセスするか、アクセス修飾子を変更
Cannot assign to 'x' because it is a read-only property readonly プロパティへの再代入 初期化時にのみ値を設定する設計にする
Class 'X' incorrectly implements interface 'Y' implements 宣言したインターフェースのメンバーが未実装 インターフェースの全メンバーを実装する
Cannot create an instance of an abstract class abstract class を直接 new している 具象サブクラスを作成して new する
this が undefined メソッドをコールバックとして渡した際に this が失われる アロー関数で定義するか .bind(this)
Non-abstract class does not implement inherited abstract member 抽象クラスのサブクラスで abstract メソッドが未実装 すべての abstract メソッドを実装する

Property has no initializer の対処

プロパティ初期化エラーの対処法
class Example {
  // 方法1: コンストラクタで初期化
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  // 方法2: 初期値を設定
  status: string = 'active';

  // 方法3: definite assignment assertion(!)
  // 「後から必ず初期化する」と TypeScript に伝える
  data!: string;

  // 方法4: オプショナルプロパティ
  description?: string;
}

this が undefined になる問題

this が失われる問題と対処法
class Timer {
  private count = 0;

  // NG: 通常のメソッドはコールバックで this が失われる
  incrementBad(): void {
    this.count++; // this が undefined になりうる
  }

  // OK: アロー関数プロパティで this を束縛
  incrementGood = (): void => {
    this.count++; // this は常にインスタンスを指す
  };

  getCount(): number {
    return this.count;
  }
}

const timer = new Timer();
const fn = timer.incrementGood;
fn(); // OK: this が Timer インスタンスを指す
console.log(timer.getCount()); // 1

まとめ

この記事では、TypeScriptのクラスの型定義を基本から実務レベルまで体系的に解説しました。最後に、各セクションの要点を整理します。

トピック ポイント
基本のクラス定義 プロパティは事前宣言 + 型注釈。メソッドの戻り値は型推論に任せてもOK
アクセス修飾子 public=どこでも、private=クラス内のみ、protected=サブクラスまで、readonly=変更不可
# vs private private はコンパイル時のみ、# は実行時も真のプライベート
パラメータプロパティ 修飾子付きコンストラクタ引数でプロパティ宣言 + 初期化を省略
getter / setter バリデーションや計算プロパティの実装に活用。getter のみ = readonly
static メンバー ユーティリティ、ファクトリメソッド、定数に使う。private constructor でインスタンス化禁止
implements インターフェースの契約を実装。複数同時 implements 可能
extends(継承) 単一継承 + override キーワードでメソッドオーバーライドを明示
abstract class 共通実装 + 抽象メソッドを強制。テンプレートメソッドパターンに最適
ジェネリクスクラス 型パラメータで汎用クラスを定義。extends 制約で安全にプロパティアクセス
構造的部分型 同じ構造なら型互換。ただし private メンバーは同じ出自が必要
Mixin 高階関数で複数クラスの機能を合成。単一継承の制限を克服
デザインパターン Singleton、Builder、Observerを型安全に実装

TypeScriptのクラスは、JavaScriptのクラスに型システムの力を加えることで、安全で保守性の高いOOPコードを書くための強力なツールです。基本的なアクセス修飾子やインターフェース実装をマスターしたら、ジェネリクスクラスやデザインパターンにも挑戦してみましょう。

TypeScript シリーズ記事