TypeScriptにおいて、クラスからインスタンスを生成する際に呼び出される「コンストラクタ(constructor)」は、オブジェクトの初期状態を決定する極めて重要な役割を担います。

JavaScriptの構文をベースにしながらも、TypeScript独自の静的型付けやアクセス修飾子を組み合わせることで、より安全で簡潔なコードを記述することが可能になっています。

本記事では、コンストラクタの基本的な書き方から、コードの記述量を劇的に減らす「引数プロパティ宣言」の仕組み、そしてアクセス修飾子を用いたカプセル化の実装手法までを詳しく解説します。

コンストラクタの基本的な役割と定義方法

コンストラクタとは、new演算子を使用してクラスのインスタンスを作成する時に実行される特別なメソッドです。

主にインスタンス変数の初期化や、オブジェクトの生成時に必要な前処理を行うために使用されます。

TypeScriptでは、コンストラクタの引数に対して型定義を行うことで、不正な値が渡されるのをコンパイル時に防ぐことができます。

まずは、最も標準的なコンストラクタの記述例を見てみましょう。

TypeScript
class User {
  // プロパティの宣言
  name: string;
  age: number;

  // コンストラクタの定義
  constructor(name: string, age: number) {
    // メンバ変数への代入
    this.name = name;
    this.age = age;
    console.log("ユーザーのインスタンスを作成しました。");
  }

  displayProfile() {
    console.log(`名前: ${this.name}, 年齢: ${this.age}`);
  }
}

// インスタンスの生成
const user1 = new User("田中太郎", 25);
user1.displayProfile();
実行結果
ユーザーのインスタンスを作成しました。
名前: 田中太郎, 年齢: 25

この例では、クラス内でプロパティを宣言し、コンストラクタの中で受け取った値をthisキーワードを用いて代入しています。

これはJavaScriptでも見られる一般的な書き方ですが、プロパティの宣言、コンストラクタの引数、代入処理の3箇所に同じ名前を記述する必要があるため、コードが冗長になりやすいという欠点があります。

引数プロパティ宣言によるコードの簡略化

TypeScriptには、コンストラクタの引数にアクセス修飾子を付与することで、プロパティの宣言と初期化を同時に行う「引数プロパティ宣言(Parameter Properties)」という非常に便利な機能があります。

この機能を利用すると、クラスのメンバ変数として明示的に定義したり、コンストラクタ内でthis.name = name;のような代入処理を書いたりする必要がなくなります。

開発現場では、コードの可読性を高め、タイプミスを減らすためにこの手法が推奨されることが多くあります。

TypeScript
class ModernUser {
  // コンストラクタの引数に修飾子を付けるだけでプロパティになる
  constructor(public name: string, private age: number) {
    // 内部での代入処理は不要
  }

  showInfo() {
    console.log(`${this.name}さんの年齢は${this.age}歳です。`);
  }
}

const user2 = new ModernUser("佐藤花子", 30);
user2.showInfo();
console.log(user2.name); // publicなのでアクセス可能
// console.log(user2.age); // エラー:privateなので外部からアクセス不可
実行結果
佐藤花子さんの年齢は30歳です。
佐藤花子

上記のコードでは、コンストラクタの引数にpublicprivateを付与しています。

これにより、TypeScriptコンパイラは自動的にクラスのプロパティとしてこれらを定義し、受け取った値をインスタンスに格納します。

コードが非常にスッキリし、メンテナンス性が向上するのが大きなメリットです。

コンストラクタにおけるアクセス修飾子の活用

コンストラクタの引数やクラスプロパティにアクセス修飾子を適切に設定することは、堅牢なアプリケーションを作る上で欠かせません。

TypeScriptでは、主に3つのアクセス修飾子を使用できます。

修飾子説明
publicどこからでもアクセス可能です。修飾子を省略した場合のデフォルトです。
privateそのクラスの内部からのみアクセス可能です。外部や継承先からは見えません。
protectedそのクラスおよび継承したサブクラスからアクセス可能です。

private修飾子によるデータの保護

オブジェクトの内部状態を外部から直接変更されたくない場合は、privateを使用します。

例えば、銀行口座の残高のような重要なデータは、必ずメソッドを経由して操作させるべきです。

TypeScript
class BankAccount {
  // 残高をprivateで保護
  constructor(private balance: number) {}

  deposit(amount: number) {
    if (amount > 0) {
      this.balance += amount;
      console.log(`${amount}円入金しました。現在の残高は${this.balance}円です。`);
    }
  }
}

const myAccount = new BankAccount(1000);
myAccount.deposit(500);
// myAccount.balance = 1000000; // コンパイルエラーになる
実行結果
500円入金しました。現在の残高は1500円です。

readonly修飾子による不変性の確保

コンストラクタで値を設定した後、その値を二度と変更したくない場合にはreadonly修飾子を併用します。

これにより、インスタンス化のタイミングでのみ値が決定され、以降の書き換えを禁止することができます。

TypeScript
class Product {
  // IDは後から変更できないようにする
  constructor(public readonly id: string, public name: string) {}
}

const item = new Product("P-001", "ワイヤレスマウス");
// item.id = "P-002"; // エラー:読み取り専用プロパティのため代入不可
item.name = "ゲーミングマウス"; // これは可能

継承におけるコンストラクタとsuper()の挙動

クラスを継承(inheritance)する場合、子クラス(サブクラス)でコンストラクタを定義する際には注意が必要です。

子クラスにコンストラクタを記述する場合、必ず最初にsuper()を呼び出さなければなりません

super()は親クラスのコンストラクタを実行するための命令であり、これを呼び出す前にthisにアクセスするとエラーが発生します。

TypeScript
class Animal {
  constructor(public species: string) {
    console.log(`動物(${species})が生成されました。`);
  }
}

class Dog extends Animal {
  // 子クラス独自のプロパティを追加
  constructor(public name: string) {
    // 親クラスのコンストラクタを呼び出す
    super("犬");
    console.log(`名前は${this.name}です。`);
  }
}

const myDog = new Dog("ポチ");
実行結果
動物(犬)が生成されました。
名前はポチです。

もし子クラスでコンストラクタを完全に省略した場合は、自動的に親クラスのコンストラクタが呼び出されます。

しかし、子クラス独自の初期化処理や追加の引数が必要な場合は、上記のように明示的なsuper()の記述が必須となります。

コンストラクタのオーバーロード

TypeScriptでは、引数のパターンが異なる複数のコンストラクタを定義する「オーバーロード」が可能です。

ただし、JavaやC#などの言語とは異なり、実装を持つコンストラクタは1つだけという制約があります。

複数のシグネチャを宣言し、実際の実装の中で引数の型や数に応じて処理を分岐させる形式をとります。

TypeScript
class Point {
  x: number;
  y: number;

  // コンストラクタのシグネチャ(定義)
  constructor(x: number, y: number);
  constructor(s: string);
  
  // 実際の実装(全パターンをカバーできる型にする)
  constructor(arg1: any, arg2?: any) {
    if (typeof arg1 === "string") {
      const parts = arg1.split(",");
      this.x = parseInt(parts[0]);
      this.y = parseInt(parts[1]);
    } else {
      this.x = arg1;
      this.y = arg2;
    }
  }
}

const p1 = new Point(10, 20);
const p2 = new Point("30,40");
console.log(`p1: ${p1.x}, ${p1.y}`);
console.log(`p2: ${p2.x}, ${p2.y}`);
実行結果
p1: 10, 20
p2: 30, 40

オーバーロードを使用することで、インスタンス化の方法に柔軟性を持たせつつ、利用者に提供するインターフェースを型安全に保つことができます。

コンストラクタ内でのバリデーションと初期化ロジック

コンストラクタは単に値を代入するだけでなく、生成されるオブジェクトが「正しい状態」であることを保証する役割も持ちます。

不適切な引数が渡された場合にエラーをスローすることで、バグを未然に防ぐことができます。

TypeScript
class UserProfile {
  constructor(public username: string) {
    // バリデーションの実施
    if (username.length === 0) {
      throw new Error("ユーザー名は1文字以上である必要があります。");
    }
    if (username.length > 20) {
      throw new Error("ユーザー名が長すぎます。");
    }
  }
}

try {
  const invalidUser = new UserProfile("");
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);
  }
}
実行結果
ユーザー名は1文字以上である必要があります。

このように、インスタンスが生成される瞬間に制約を設けることは、ドメイン主導設計(DDD)などの実践的な手法においても非常に重要視されます。

ただし、コンストラクタ内でAPI通信や複雑な計算などの「重い処理」を行うことは避けるべきです。

コンストラクタは迅速にインスタンスを返すことに専念し、副作用のある処理は別途メソッドを用意することを検討してください。

まとめ

TypeScriptのコンストラクタは、クラスベースのオブジェクト指向開発における中核的な要素です。

従来のJavaScriptのような冗長な記述を避けたい場合は、「引数プロパティ宣言」を活用することで、コードを大幅に簡略化できます。

また、publicprivateprotectedといったアクセス修飾子やreadonlyを使い分けることで、安全なデータの隠蔽と不変性を実現できます。

継承を利用する際のsuper()の呼び出しルールや、柔軟なインスタンス化を可能にするオーバーロードといった応用テクニックも、大規模な開発では欠かせません。

本記事で紹介した基本をマスターし、型安全でメンテナンス性の高いTypeScriptコードの記述に役立ててください。