はじめに

TypeScriptでアプリケーションを開発する際、クラスはオブジェクト指向プログラミング(OOP)の中核を担う重要な機能です。JavaScriptのES2015で導入されたクラス構文に、TypeScriptは型注釈やアクセス修飾子などの強力な機能を追加しています。

本記事では、TypeScriptのクラスの基本構文、コンストラクタの型定義、アクセス修飾子(publicprivateprotected)、readonly修飾子、そして抽象クラス(abstract)について詳しく解説します。

この記事を読み終えると、以下のことができるようになります。

  • TypeScriptのクラスを型安全に定義できる
  • コンストラクタで引数の型を適切に定義できる
  • アクセス修飾子を使ってカプセル化を実現できる
  • readonlyで変更不可のプロパティを定義できる
  • 抽象クラスを使って継承の設計ができる

実行環境・前提条件

前提知識

  • JavaScriptのクラス構文の基本(constructor、extends、superなど)
  • TypeScriptの基本的な型注釈の書き方(基本型入門を参照)
  • interfaceの基本的な使い方(オブジェクト型入門を参照)

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • 型定義に合致しない値のコンストラクタ引数渡しがコンパイルエラーになる
  • privateprotectedメンバーへの外部からのアクセスがコンパイルエラーになる
  • readonlyプロパティへの再代入がコンパイルエラーになる
  • 抽象クラスの直接インスタンス化がコンパイルエラーになる

TypeScriptクラスの基本構文

クラスの定義とフィールド

TypeScriptのクラスは、JavaScriptのクラス構文に型注釈を追加して定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Point {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

const point = new Point(10, 20);
console.log(point.x); // 10
console.log(point.y); // 20

フィールドには型注釈を付けることで、型安全性を確保できます。TypeScriptのstrictPropertyInitializationオプションが有効な場合、フィールドはコンストラクタ内で初期化するか、初期値を設定する必要があります。

フィールドの初期化

フィールドは宣言時に初期値を設定できます。初期値がある場合、TypeScriptは型を推論します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Counter {
  count = 0; // number型と推論される

  increment(): void {
    this.count++;
  }

  decrement(): void {
    this.count--;
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.count); // 1

メソッドの定義

クラス内のメソッドには、引数と戻り値に型注釈を付けられます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Calculator {
  add(a: number, b: number): number {
    return a + b;
  }

  subtract(a: number, b: number): number {
    return a - b;
  }

  multiply(a: number, b: number): number {
    return a * b;
  }

  divide(a: number, b: number): number {
    if (b === 0) {
      throw new Error("0で除算はできません");
    }
    return a / b;
  }
}

const calc = new Calculator();
console.log(calc.add(5, 3));      // 8
console.log(calc.multiply(4, 2)); // 8

コンストラクタの型定義

基本的なコンストラクタ

コンストラクタの引数には型注釈を付けて、インスタンス生成時の型安全性を確保します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class User {
  name: string;
  age: number;
  email: string;

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

const user = new User("田中太郎", 30, "tanaka@example.com");

// 型が合わない場合はコンパイルエラー
// const invalidUser = new User("佐藤", "30歳", "sato@example.com");
// エラー: Argument of type 'string' is not assignable to parameter of type 'number'.

オプショナル引数とデフォルト値

コンストラクタ引数にはオプショナル引数(?)やデフォルト値を設定できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Product {
  name: string;
  price: number;
  description: string;

  constructor(name: string, price: number, description: string = "説明なし") {
    this.name = name;
    this.price = price;
    this.description = description;
  }
}

const product1 = new Product("ノートPC", 150000, "高性能ラップトップ");
const product2 = new Product("マウス", 3000); // descriptionはデフォルト値

console.log(product1.description); // 高性能ラップトップ
console.log(product2.description); // 説明なし

パラメータプロパティ

TypeScriptには、コンストラクタ引数を直接クラスプロパティに変換する「パラメータプロパティ」という便利な構文があります。アクセス修飾子またはreadonlyを引数に付けると、自動的にプロパティとして定義されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 従来の書き方
class UserOld {
  name: string;
  age: number;

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

// パラメータプロパティを使った書き方
class UserNew {
  constructor(
    public name: string,
    public age: number
  ) {
    // コンストラクタ本体は空でOK
  }
}

const user = new UserNew("山田花子", 25);
console.log(user.name); // 山田花子
console.log(user.age);  // 25

パラメータプロパティを使うと、フィールド宣言と初期化を1行で記述でき、コードが簡潔になります。

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

TypeScriptのクラスには、メンバーの可視性を制御する3つのアクセス修飾子があります。

public(公開)

publicはデフォルトのアクセス修飾子で、どこからでもアクセス可能です。明示的に書かなくてもすべてのメンバーはpublicになりますが、意図を明確にするために記述することもあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Animal {
  public name: string;

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

  public speak(): void {
    console.log(`${this.name}が鳴いています`);
  }
}

const animal = new Animal("ポチ");
console.log(animal.name); // ポチ(外部からアクセス可能)
animal.speak();           // ポチが鳴いています

private(非公開)

privateメンバーは、そのクラス内からのみアクセスできます。サブクラスや外部からはアクセスできません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class BankAccount {
  private balance: number;

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

  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
      console.log(`${amount}円を入金しました。残高: ${this.balance}円`);
    }
  }

  public withdraw(amount: number): boolean {
    if (amount > 0 && amount <= this.balance) {
      this.balance -= amount;
      console.log(`${amount}円を出金しました。残高: ${this.balance}円`);
      return true;
    }
    console.log("出金できませんでした");
    return false;
  }

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

const account = new BankAccount(10000);
account.deposit(5000);  // 5000円を入金しました。残高: 15000円
account.withdraw(3000); // 3000円を出金しました。残高: 12000円

// 外部から直接アクセスするとコンパイルエラー
// console.log(account.balance);
// エラー: Property 'balance' is private and only accessible within class 'BankAccount'.

protected(保護)

protectedメンバーは、そのクラスとサブクラス(継承先)からアクセスできますが、外部からはアクセスできません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person {
  protected name: string;

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

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

class Employee extends Person {
  private department: string;

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

  public introduce(): string {
    // protectedメンバーにはサブクラスからアクセス可能
    return `${this.greet()}${this.department}で働いています。`;
  }
}

const employee = new Employee("鈴木一郎", "開発部");
console.log(employee.introduce());
// こんにちは、鈴木一郎です。開発部で働いています。

// 外部からはアクセス不可
// console.log(employee.name);
// エラー: Property 'name' is protected and only accessible within class 'Person' and its subclasses.

アクセス修飾子の比較表

修飾子 クラス内 サブクラス 外部
public
protected 不可
private 不可 不可

JavaScriptのプライベートフィールド(#)との違い

TypeScriptのprivateは型チェック時のみ有効で、コンパイル後のJavaScriptでは通常のプロパティになります。一方、JavaScriptのプライベートフィールド(#)は実行時にも真のプライベートを保証します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Example {
  private tsPrivate: string = "TypeScriptのprivate";
  #jsPrivate: string = "JavaScriptのプライベートフィールド";

  public showValues(): void {
    console.log(this.tsPrivate);
    console.log(this.#jsPrivate);
  }
}

const example = new Example();
example.showValues();

// TypeScriptのprivateはブラケット記法でアクセス可能(型エラーは出る)
// console.log(example["tsPrivate"]); // 実行時は動作する

// JavaScriptの#privateは実行時もアクセス不可
// console.log(example.#jsPrivate); // SyntaxError

厳密なプライバシーが必要な場合は、JavaScriptのプライベートフィールド(#)の使用を検討してください。

readonly修飾子

基本的な使い方

readonly修飾子を付けたプロパティは、初期化後に変更できなくなります。コンストラクタ内での初期化か、宣言時の初期値設定のみ許可されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Config {
  readonly apiUrl: string;
  readonly maxRetries: number = 3;

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

const config = new Config("https://api.example.com");
console.log(config.apiUrl);     // https://api.example.com
console.log(config.maxRetries); // 3

// 再代入しようとするとコンパイルエラー
// config.apiUrl = "https://other.example.com";
// エラー: Cannot assign to 'apiUrl' because it is a read-only property.

パラメータプロパティとの組み合わせ

readonlyはパラメータプロパティと組み合わせて使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class ImmutablePoint {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}

  // 新しいPointを返すメソッド(元のインスタンスは変更しない)
  translate(dx: number, dy: number): ImmutablePoint {
    return new ImmutablePoint(this.x + dx, this.y + dy);
  }
}

const point1 = new ImmutablePoint(10, 20);
const point2 = point1.translate(5, 5);

console.log(point1.x, point1.y); // 10 20(元のインスタンスは変更されない)
console.log(point2.x, point2.y); // 15 25

readonlyとconstの違い

readonlyはプロパティに対して使用し、constは変数に対して使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Container {
  readonly items: string[] = [];
}

const container = new Container();

// 配列の中身は変更可能(readonlyはプロパティの再代入を防ぐだけ)
container.items.push("item1");
container.items.push("item2");
console.log(container.items); // ["item1", "item2"]

// プロパティ自体の再代入は不可
// container.items = [];
// エラー: Cannot assign to 'items' because it is a read-only property.

配列やオブジェクトの中身まで不変にしたい場合は、ReadonlyArray<T>Readonly<T>ユーティリティ型を使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ImmutableContainer {
  readonly items: ReadonlyArray<string>;

  constructor(items: string[]) {
    this.items = items;
  }
}

const container = new ImmutableContainer(["a", "b", "c"]);
// container.items.push("d");
// エラー: Property 'push' does not exist on type 'readonly string[]'.

抽象クラス(abstract)

抽象クラスの基本

抽象クラスは、直接インスタンス化できないクラスです。サブクラスのベース(基底クラス)として使用し、共通の実装を提供しつつ、一部のメソッドはサブクラスでの実装を強制できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
abstract class Shape {
  constructor(public name: string) {}

  // 抽象メソッド: サブクラスで必ず実装する必要がある
  abstract getArea(): number;

  // 通常のメソッド: サブクラスでそのまま使用できる
  describe(): string {
    return `${this.name}の面積は${this.getArea()}です`;
  }
}

// 抽象クラスは直接インスタンス化できない
// const shape = new Shape("図形");
// エラー: Cannot create an instance of an abstract class.

抽象クラスの継承

抽象クラスを継承するサブクラスは、すべての抽象メソッドを実装する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
abstract class Shape {
  constructor(public name: string) {}

  abstract getArea(): number;

  describe(): string {
    return `${this.name}の面積は${this.getArea()}です`;
  }
}

class Circle extends Shape {
  constructor(public radius: number) {
    super("円");
  }

  // 抽象メソッドの実装
  getArea(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Shape {
  constructor(
    public width: number,
    public height: number
  ) {
    super("長方形");
  }

  // 抽象メソッドの実装
  getArea(): number {
    return this.width * this.height;
  }
}

const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);

console.log(circle.describe());    // 円の面積は78.53981633974483です
console.log(rectangle.describe()); // 長方形の面積は24です

抽象プロパティ

メソッドだけでなく、プロパティも抽象として定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
abstract class Vehicle {
  abstract readonly wheels: number;
  abstract readonly brand: string;

  abstract start(): void;

  displayInfo(): void {
    console.log(`${this.brand}: ${this.wheels}輪`);
  }
}

class Car extends Vehicle {
  readonly wheels = 4;
  readonly brand: string;

  constructor(brand: string) {
    super();
    this.brand = brand;
  }

  start(): void {
    console.log(`${this.brand}のエンジンを始動します`);
  }
}

class Motorcycle extends Vehicle {
  readonly wheels = 2;

  constructor(public readonly brand: string) {
    super();
  }

  start(): void {
    console.log(`${this.brand}のエンジンをキックスタートします`);
  }
}

const car = new Car("トヨタ");
const bike = new Motorcycle("ホンダ");

car.displayInfo();  // トヨタ: 4輪
bike.displayInfo(); // ホンダ: 2輪

car.start();  // トヨタのエンジンを始動します
bike.start(); // ホンダのエンジンをキックスタートします

抽象クラスとinterfaceの使い分け

抽象クラスとinterfaceはどちらも型の契約を定義しますが、使い分けの指針があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// interface: 純粋な型の契約(実装を持たない)
interface Printable {
  print(): void;
}

// 抽象クラス: 共通の実装を含む基底クラス
abstract class Document {
  constructor(public title: string) {}

  abstract getContent(): string;

  // 共通の実装を提供
  printHeader(): void {
    console.log(`=== ${this.title} ===`);
  }
}

class Report extends Document implements Printable {
  constructor(
    title: string,
    private body: string
  ) {
    super(title);
  }

  getContent(): string {
    return this.body;
  }

  print(): void {
    this.printHeader();
    console.log(this.getContent());
  }
}

const report = new Report("月次レポート", "今月の売上は...");
report.print();
// === 月次レポート ===
// 今月の売上は...

抽象クラスを使う場合

  • 共通の実装(メソッドやプロパティ)を提供したい
  • 状態(フィールド)を持つ基底クラスが必要
  • コンストラクタで初期化処理を共有したい

interfaceを使う場合

  • 純粋に型の契約のみを定義したい
  • 複数のinterfaceを実装したい(多重継承のような効果)
  • 異なる継承階層のクラスに共通の型を付けたい

実践的なクラス設計例

ここまで学んだ内容を組み合わせた実践的な例を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 抽象クラス: ログ出力の基底クラス
abstract class Logger {
  constructor(protected readonly prefix: string) {}

  abstract log(message: string): void;

  protected formatMessage(message: string): string {
    const timestamp = new Date().toISOString();
    return `[${timestamp}] [${this.prefix}] ${message}`;
  }
}

// コンソールへのログ出力
class ConsoleLogger extends Logger {
  constructor(prefix: string = "INFO") {
    super(prefix);
  }

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

// ファイルへのログ出力(シミュレーション)
class FileLogger extends Logger {
  private logs: string[] = [];

  constructor(
    prefix: string,
    private readonly filename: string
  ) {
    super(prefix);
  }

  log(message: string): void {
    const formatted = this.formatMessage(message);
    this.logs.push(formatted);
    console.log(`[File: ${this.filename}] ${formatted}`);
  }

  public getLogs(): ReadonlyArray<string> {
    return this.logs;
  }
}

// アプリケーションクラス
class Application {
  private readonly logger: Logger;

  constructor(
    public readonly name: string,
    logger: Logger
  ) {
    this.logger = logger;
  }

  start(): void {
    this.logger.log(`${this.name}を起動しました`);
  }

  stop(): void {
    this.logger.log(`${this.name}を停止しました`);
  }
}

// 使用例
const consoleLogger = new ConsoleLogger("APP");
const fileLogger = new FileLogger("DEBUG", "app.log");

const app1 = new Application("WebServer", consoleLogger);
const app2 = new Application("BatchProcessor", fileLogger);

app1.start();
// [2026-01-03T11:00:00.000Z] [APP] WebServerを起動しました

app2.start();
// [File: app.log] [2026-01-03T11:00:00.000Z] [DEBUG] BatchProcessorを起動しました

app2.stop();
// [File: app.log] [2026-01-03T11:00:00.000Z] [DEBUG] BatchProcessorを停止しました

console.log(fileLogger.getLogs());
// 保存されたログの配列を表示

この例では、以下のTypeScriptのクラス機能を活用しています。

  • 抽象クラス: Loggerで共通のログフォーマット処理を提供
  • protected: サブクラスからのみアクセス可能なprefixformatMessage
  • private: FileLogger内部でのみ使用するlogs配列
  • readonly: 変更されるべきでないprefixfilename
  • パラメータプロパティ: コンストラクタ引数を簡潔にプロパティ化

よくある間違いと解決策

strictPropertyInitializationエラー

strictモードでは、フィールドの初期化忘れがエラーになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// エラー: Property 'name' has no initializer and is not definitely assigned in the constructor.
class BadExample {
  name: string; // 初期化されていない
}

// 解決策1: コンストラクタで初期化
class GoodExample1 {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 解決策2: 初期値を設定
class GoodExample2 {
  name: string = "";
}

// 解決策3: 明確な代入アサーション(外部で初期化される場合)
class GoodExample3 {
  name!: string; // 「必ず初期化される」と宣言
}

thisの参照問題

クラスメソッドをコールバックとして渡すと、thisの参照が失われることがあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Button {
  label: string = "Click me";

  // 通常のメソッド: thisの参照が失われる可能性あり
  handleClick(): void {
    console.log(this.label);
  }

  // アロー関数プロパティ: thisが自動的にバインドされる
  handleClickArrow = (): void => {
    console.log(this.label);
  };
}

const button = new Button();

// 問題のあるケース
const callback = button.handleClick;
// callback(); // エラー: Cannot read property 'label' of undefined

// 解決策1: bindを使用
const boundCallback = button.handleClick.bind(button);
boundCallback(); // Click me

// 解決策2: アロー関数プロパティを使用
const arrowCallback = button.handleClickArrow;
arrowCallback(); // Click me

まとめ

本記事では、TypeScriptのクラスについて以下の内容を解説しました。

  • クラスの基本構文: フィールド、メソッド、コンストラクタの型定義
  • パラメータプロパティ: コンストラクタ引数を簡潔にプロパティ化する構文
  • アクセス修飾子: publicprivateprotectedによるカプセル化
  • readonly修飾子: 変更不可のプロパティを定義
  • 抽象クラス: abstractを使った継承設計

TypeScriptのクラスを適切に使うことで、型安全で保守性の高いオブジェクト指向プログラミングが実現できます。次のステップとして、interfaceの実装(implements)で、interfaceとクラスの連携について学んでみてください。

参考リンク