TypeScriptでクラスを書こう|基礎からコード付きで徹底解説

※本サイトにはプロモーション・広告が含まれています。

(最終更新月:2023年11月)

✔当記事は以下の疑問を持つ方にオススメです

「TypeScriptのクラス構文って具体的にどんな特徴があるの?」

「クラスの書き方をTypeScriptで学びたい」

「TypeScriptクラスの活用例を見てみたい」

✔当記事で解説する主な内容

  • TypeScriptクラスの基礎知識
  • クラスの定義から継承までのTypeScript特有の文法
  • 実践的なTypeScriptクラス利用方法

当記事では、TypeScriptにおけるクラスの基本から始めて、効果的な文法や高度な機能まで、具体的なコード例を交えながら明快にご紹介します。

初心者でも理解しやすいようにポイントをしっかり押さえていますので、ぜひ最後までご覧ください。

筆者プロフィール

筆者プロフィールアイコン

【現職】プロダクトマネージャー

【副業】ブログ(月間20万PV)/YouTube/Web・アプリ制作

「プログラミング × ライティング × 営業」の経験を活かし、30後半からのIT系職へシフト。現在はプロダクトマネージャーとして、さまざまな関係者の間に入り奮闘してます。当サイトでは、実際に手を動かせるWebアプリの開発を通じて、プログラミングはもちろん、IT職に必要な情報を提供していきます。

【当ブログで紹介しているサイト】

当サイトチュートリアルで作成したデモ版日報アプリ

Django × Reactで開発したツール系Webアプリ

✔人に見せても恥ずかしくないコードを書こう

「リーダブルコード」は、わかりやすく良いコードの定義を教えてくれる本です。

  • 見るからにきれいなコードの書き方
  • コードの分割方法
  • 変数や関数の命名規則

エンジニアのスタンダートとすべき基準を一から解説しています。

何回も読むのに値する本なので、ぜひ手にとって読んでみてください。

クラスの基本

こちらでは、TypeScriptにおける「クラスの基本」についてお伝えしていきます。

「クラスの基本」を理解することで、大規模なプロジェクトのコード管理や再利用に役立つでしょう。

  • TypeScriptでのクラスの構文
  • クラスのインスタンス化について
  • コンストラクタの詳細な解説

TypeScriptでのクラスの構文

TypeScriptではクラスの定義がシンプルな構文で表現できます。

以下のコードは基本的なクラスPersonを定義している例です。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

このクラスには以下が含まれています。

  • nameというパブリックプロパティ
  • クラスのインスタンスを作る際に必ず呼び出されるconstructorメソッド
  • greetという独自のメソッド

初心者にも分かりやすく、クラスの基本構造を体系的に把握していきましょう。

クラスのインスタンス化について

クラスからオブジェクトを生成するには、newキーワードを使用してインスタンス化します。

以下のコードは、PersonクラスからpersonInstanceという新しいインスタンスを作成する例です。

const personInstance = new Person('Alice');
personInstance.greet(); // "Hello, my name is Alice"とコンソールに表示される

このインスタンス化プロセスにより、クラスに定義された構造、メソッド、プロパティが個々のオブジェクトに割り当てられます。

新しく作られたpersonInstanceオブジェクトは、Personクラスのすべての機能を持つ独立した実体です。

コンストラクタの詳細な解説

コンストラクタは、クラスがインスタンス化された時に自動的に呼ばれる特別なメソッド。

クラスに初期データを設定するために使われます。

コンストラクタの中で、thisキーワードを使い、クラス内のプロパティにアクセス可能。

引数から受け取った値をプロパティに割り当てます。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name; // コンストラクタでプロパティに値を割り当てる
  }
}

このnameプロパティは、クラスの外部からもアクセス可能なパブリックプロパティです。

必要に応じてアクセス修飾子を使用してアクセス範囲の制限も可能。

コンストラクタは非常に強力な初期化ツールなので、その使い方を正確に理解することが重要です。

クラスのプロパティ

クラス内の「プロパティ」に着目し、その宣言、初期化、アクセス方法について深堀りしていきます。

プロパティを適切に扱うことは、データ保護とクラス設計において不可欠です。

  • プロパティの宣言と初期化
  • プロパティのアクセスと保護
  • プロパティ初期化子の使い方と長所

プロパティの宣言と初期化

TypeScriptのクラスでプロパティを宣言する際には、通常、その型アノテーションも指定します。

宣言時にプロパティにデフォルト値を設定可能です。

class Person {
  name: string = 'Unknown'; // プロパティの宣言と同時に初期値を設定
}

このデフォルト値は、インスタンス化時にコンストラクタを通じて、新しい値が割り当てられるまでの一時的な値です。

初期化により、型の安全性を確保し、未定義のプロパティアクセスを防ぎます。

プロパティのアクセスと保護

クラスのプロパティにはそれぞれアクセス範囲があります。

以下の2つから指定可能です。

  • public(どこからでもアクセス可能)
  • private(クラス内からのみアクセス可能
  • protected(派生クラスからもアクセス可能
class Person {
  private secret: string;
  constructor(secret: string) {
    this.secret = secret; // privateプロパティはクラス内部からのみアクセス可能
  }
}

厳格なアクセス制御を施すことは不正なデータ操作を防ぎ、オブジェクトの整合性を維持するのに役立ちます。

プロパティ初期化子の使い方と長所

プロパティ初期化子を使用すると、クラスのプロパティを直接初期化できます。

コンストラクタの記述を簡潔に保てるのがメリットです。

class Person {
  name = 'Unknown'; // コンストラクタを用いずに初期化
}

プロパティ初期化子を使用すると、個々のインスタンスに固有の初期値を提供しません。

全インスタンスで共通の値を設定する際の冗長なコードを避けられます。

アクセス制御

「アクセス制御」には、クラスのメンバに対するアクセスレベルを指定する修飾子が含まれます。

データのカプセル化とセキュリティを強化するために、これらの制御機能を理解することは必須です。

  • アクセス修飾子の種類と適用
  • アクセス制限の実務への応用
  • readonly修飾子の活用例

アクセス修飾子の種類と適用

クラスのメンバに適用できるアクセス修飾子にはpublicprivateprotectedがあり、それぞれがメンバのアクセス範囲を制御します。

class Person {
  public name: string;         // どこからでもアクセス可能
  private age: number;         // クラス内部からのみアクセス可能
  protected nickname: string;  // このクラスと派生クラスからアクセス可能
}

これらのアクセス修飾子を適切に選び、データの公開範囲を制限することで、オブジェクトの安全性を高めます。

アクセス制限の実務への応用

実際のアプリケーション開発において、アクセス制御は不可欠

私的なデータやメソッドは外部に公開すべきではなく、必要なオペレーションのみをクラスの公開インターフェイスとして設計すべきです。

class Person {
  public greet(): void {
    console.log("Hello!");
  }
  private calculateAge(): void {
    //何らかの計算処理
  }
}

greetメソッドが公開インターフェイスとして提供され、calculateAgeメソッドは内部使用のために隠されています。

アクセス制御の原則を適用することで、クラスが外部からどのように使われるかを効果的に管理できます。

readonly修飾子の活用例

readonly修飾子を使用すると、プロパティの値が宣言後に変更されることがないことを保証できます。

readonlyプロパティは宣言時またはコンストラクタ内でのみ値が割り当て可能です。

class Person {
  readonly birthDate: Date;
  constructor(birthDate: Date) {
    this.birthDate = birthDate;
  }
}

このbirthDateプロパティは、Personオブジェクトの創造後には不変となり、オブジェクトの一貫性を保つために役立ちます。

データの不変性が重要な場面では、readonlyの使用を積極的に検討しましょう。

クラスの継承

「クラスの継承」では、コードの再利用性を高め、階層的なクラス構造を作り出すためのメカニズムを提供します。

クラスの継承は、複雑なオブジェクト指向プログラムがより管理しやすくなるでしょう。

  • 継承の仕組みとそのメリット
  • サブクラスの作成手順と注意点
  • メソッドのオーバーライドとポリモーフィズム

継承の仕組みとそのメリット

TypeScriptでは、extendsキーワードによってクラスの継承がおこなわれます。

サブクラスはスーパークラスのプロパティやメソッドを継承し、それに追加の機能を提供することが可能です。

class Employee extends Person {
  department: string;
}

この例では、PersonスーパークラスからEmployeeサブクラスが派生しています。

EmployeePersonのすべての機能を継承しつつdepartmentという新たなプロパティを持てるのです。

継承はコードの重複を減らし、保守性を向上させるために大変有効です。

サブクラスの作成手順と注意点

サブクラスの作成では、スーパークラスのコンストラクタを呼び出すためにsuperキーワードを使用しましょう。

継承する際のプロパティやメソッドの名前衝突にも注意が必要です。

class Employee extends Person {
  department: string;
  constructor(name: string, department: string) {
    super(name); // スーパークラスのコンストラクタを呼び出す
    this.department = department;
  }
}

サブクラスEmployeeでは、super()を通じてスーパークラスPersonのコンストラクタを先に呼び出した後で、サブクラス独自のプロパティを設定しています。

この手段により、スムーズなクラス間の連携が可能です。

メソッドのオーバーライドとポリモーフィズム

ポリモーフィズムは、サブクラスがスーパークラスのメソッドを独自の振る舞いでオーバーライドすることを指します。

異なるクラスオブジェクトが、同一のインターフェイスでさまざまな動作を実現できるのです。

class Employee extends Person {
  department: string;
  greet() {
    console.log(`Hello, my name is ${this.name} and I work in ${this.department}.`);
  }
}

このEmployeeクラスはPersonクラスのgreetメソッドをオーバーライドして、デパートメントの情報も出力するよう拡張しています。

同じgreetメソッドでも、オブジェクトの型によって異なる結果を提供することができ、プログラムの柔軟性が高まるのです。

詳細なクラス機能

TypeScriptには高度なクラス機能も備わっており、柔軟で再利用性の高いコード設計を促進します。

抽象クラスやインターフェイスなどの概念をマスターすると、より洗練されたクラスの設計が可能です。

  • 抽象クラスの使い道と実装
  • 抽象メソッドの定義とサブクラスへの影響
  • インターフェースを用いた契約指向の設計

抽象クラスの使い道と実装

抽象クラスはインスタンス化することができないクラスであり、特定のメソッドやプロパティの構造を定義するために使用されるもの。

継承するすべてのサブクラスに対して、特定のメソッドやプロパティの実装を強制できます。

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

Shapeという抽象クラスは具体的な形状についての詳細を定義しません。

getAreaというメソッドがサブクラスにおいて実装されることを要求しています。

抽象クラスは、テンプレートとしての役割を果たし、コンセプトを共通する設計の枠組みを提供するのです。

抽象メソッドの定義とサブクラスへの影響

抽象メソッドは、具体的な実装を持たないメソッドで、抽象クラス内で宣言されます。

サブクラスは抽象メソッドをオーバーライドすることにより、具体的な処理の提供が必要です。

abstract class Shape {
  abstract getArea(): number;
}

class Circle extends Shape {
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  // 抽象メソッドをオーバーライドして具体的な計算を行う
  getArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

CircleクラスはShapeクラスから継承し、抽象メソッドgetAreaに実際の計算処理を提供。

抽象メソッドはサブクラスが持つべき行動を定義し、インターフェイスの一貫性を保証します。

インターフェースを用いた契約指向の設計

インターフェースは、特定のメソッドやプロパティの契約を定義し、それを実装するクラスはその契約を満たす必要があります。

インターフェースを使うことで、クラス間の依存関係を簡潔かつ柔軟に保つことが可能です。

interface IShape {
  getArea(): number;
}

class Rectangle implements IShape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  // インターフェースで要求されたメソッドを実装する
  getArea(): number {
    return this.width * this.height;
  }
}

RectangleクラスはIShapeインターフェースを実装し、getAreaメソッドを提供しています。

この手法により、さまざまな形状が同じ方法で面積を計算できるという、一種の設計契約が生まれます。

静的メンバの活用

静的メンバは、クラスのインスタンスではなく、クラス自体に直接紐付いたプロパティやメソッドです。

これらを活用することで、インスタンス化される前のクラスレベルでのデータや機能を提供できます。

  • 静的プロパティとその活用シーン
  • 静的メソッドの役割と定義
  • クラスレベルのデータ管理

静的プロパティとその活用シーン

静的プロパティは、クラスの各インスタンス間で共有されるプロパティ

staticキーワードを使って宣言されるものです。

このプロパティはインスタンス化されずにクラス名から直接アクセスされるため、設定値やシングルトンパターンの実装に使われます。

class Config {
  static readonly baseUrl: string = 'https://api.example.com';

  // Config.baseUrlなどでアクセスできる
}

ConfigクラスのbaseUrlは、アプリケーション全体で共有される設定値として使用され、各インスタンスの作成なしでアクセスが可能です。

静的メソッドの役割と定義

静的メソッドもまた、クラスレベルで利用できる関数です。

これらは主にユーティリティ関数やファクトリーメソッドとして活用され、インスタンスの状態に依存しない処理をおこないます。

class MathHelper {
  static sum(a: number, b: number): number {
    return a + b;
  }
}
const result = MathHelper.sum(10, 20); // クラス名を使って静的メソッドを呼び出す

このMathHelperクラスのsumメソッドは、二つの数値の合計を返す静的メソッドとして定義され、インスタンス不要で直接呼び出しが可能です。

クラスレベルのデータ管理

静的プロパティを使ったクラスレベルのデータ管理は、アプリケーションの状態や設定を一箇所で集中管理するために有用です。

ステートフルな振る舞いを持たせたい場合や、グローバルなキャッシュを実装する際にも静的プロパティが活躍します。

class CacheStore {
  private static cache: Map<string, any> = new Map();

  static setItem(key: string, value: any): void {
    this.cache.set(key, value);
  }

  static getItem(key: string): any {
    return this.cache.get(key);
  }
}

このCacheStoreクラスでは、 cacheという静的プロパティを使ってデータのセットや取得をおこないます。

一度セットされたキャッシュは、どのインスタンスからでも参照可能です。

特殊な構造と書き方

TypeScriptのクラスでは、簡潔なシンタックスと強力な機能を提供するいくつかの特殊な記法が利用できます。

これらを使いこなすことで、コードをより読みやすく保守しやすいものにしましょう。

  • コンストラクタの省略形とその仕組み
  • getterとsetterを用いた状態のカプセル化
  • クラスフィールドの先進的な書き方

コンストラクタの省略形とその仕組み

コンストラクタの省略形を使用すると、クラスのプロパティの宣言と初期化を、コンストラクタのパラメータリスト内で簡潔におこなえます

class Person {
  constructor(public name: string, private age: number) {}
}

const bob = new Person('Bob', 29);
console.log(bob.name); // 'Bob' を出力
// bob.age は private なので外部からアクセスできない

上記のコンストラクタのパラメータにpublicおよびprivateのアクセス修飾子を使用すると、それぞれのスコープのプロパティが自動的にクラス内に作成され、パラメータの値が割り当てられます。

getterとsetterを用いた状態のカプセル化

getterとsetterを使用することで、クラスのプロパティへのアクセスをより細かく制御できます。

これにより、値の読み取りと更新のプロセスに追加のロジックを挟めるのです。

class Person {
  private _email: string;

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

  set email(newEmail: string) {
    if (/^\S+@\S+\.\S+$/.test(newEmail)) {
      this._email = newEmail;
    } else {
      throw new Error('Invalid email format.');
    }
  }
}

const alice = new Person();
alice.email = 'alice@example.com'; // 有効なメールアドレス
console.log(alice.email);
// alice.email = 'not-an-email'; // この行は例外を投げる

emailプロパティの値が有効なメールアドレス形式であることをチェックするためにsetterが使用されています。

クラスフィールドの先進的な書き方

TypeScriptでは、クラスフィールド(プロパティ)を宣言する際に、よりモダンな構文と機能を使用できます。

例えば、パブリックフィールド宣言やプライベートフィールドの宣言などを含める方法です。

class Counter {
  #count = 0; // プライベートフィールド宣言

  increment() {
    this.#count++;
  }

  getCount() {
    return this.#count;
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.getCount()); // 1
// counter.#count; // この行は外部からアクセスできず、エラーとなる

この例では、#countというプライベートフィールドがクラス内で宣言されており、外部からアクセスできません。

この機構は、クラスの内部状態を隠蔽し、カプセル化を強化するのに役立ちます。

TypeScriptの特殊なクラス機能

TypeScriptにはJavaScriptのクラスにはない、型安全性や拡張性を向上する特別な機能が数々あります。

これらの特性を利用することで、より堅牢なアプリケーションを開発できるのです。

  • 抽象クラスの適切な運用方法
  • クラスと型の関係性の解説
  • ジェネリッククラスと柔軟な型設計

抽象クラスの適切な運用方法

抽象クラスは、具体的な実装を持たせることなく、共有するべきインターフェースやメソッドのシグニチャを子クラスに伝えるために用いられます。

適正に運用することで、クラス階層の設計思想を明確にし、保守性と拡張性を高めるのです。

abstract class Component {
  abstract render(): string;

  mount(container: HTMLElement): void {
    container.innerHTML = this.render();
  }
}

class ButtonComponent extends Component {
  render(): string {
    return '<button>Click me</button>';
  }
}

Component抽象クラスはrenderメソッドのみを抽象メソッドとして定義しており、具体的なHTMLを生成する実装はButtonComponentサブクラスで提供されています。

クラスと型の関係性の解説

TypeScriptでは、クラスは型としても機能します。

クラスインスタンスの期待される形状を定義し、コンパイルタイムに型の整合性を検証することが可能です。

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

// Person 型の変数を宣言
let person: Person;
person = new Person('Alice'); // 正しい使用
// person = { name: 'Bob' }; // コンパイルエラー:リテラルは Person クラスのインスタンスではない

Personクラスのインスタンスであることが期待される変数に、Personクラスではないオブジェクトを割り当てようとすると、TypeScriptの型チェックによりエラーが発生します。

ジェネリッククラスと柔軟な型設計

ジェネリッククラスを使用すると、異なる型で動作する一貫したクラスの構造を宣言できます。

汎用的なコンポーネントやデータ構造の定義時に型の柔軟性を保つことが可能です。

class DataWrapper<T> {
  private data: T;

  constructor(data: T) {
    this.data = data;
  }

  getData(): T {
    return this.data;
  }
}

const numberWrapper = new DataWrapper<number>(10);
console.log(numberWrapper.getData()); // 10

const stringWrapper = new DataWrapper<string>('Hello, TypeScript');
console.log(stringWrapper.getData()); // 'Hello, TypeScript'

上記のDataWrapperクラスは、ジェネリック型Tを用いて定義されており、さまざまな型のデータを保持できます。

これにより、再利用性の高いデータコンテナを構築できるのです。

まとめ

当記事では、TypeScriptのクラスについて学習してきました。

基本から発展的なトピックまでカバーし、あなたの知識がより深まったことでしょう。

これらの知識は実際のアプリケーション開発において非常に重要であり、コードの品質とメンテナンス性を大きく向上させることができます。

次のステップとしては、実際のプロジェクトへの適用を通じて、これらのクラス機能の実用性を体験することがおすすめ。

さらに、TypeScriptの公式ドキュメントやオンラインチュートリアル、コミュニティフォーラムなどを活用しながら、TypeScriptの旅を続けましょう。

タイトルとURLをコピーしました