TypeScriptでクラスを学ぼう!初心者でもわかる10のコード例 – Japanシーモア

TypeScriptでクラスを学ぼう!初心者でもわかる10のコード例

TypeScriptのクラスを学ぶイラストTypeScript
この記事は約23分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

このサービスは複数のSSPによる協力の下、運営されています。

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

TypeScriptは、JavaScriptに静的な型システムとクラスベースのオブジェクト指向プログラミングを追加することで、大規模なアプリケーションの開発を効果的にサポートする言語として注目を集めています。

TypeScriptの中でも特に「クラス」という機能は、オブジェクト指向の考え方を深く理解する上で鍵となる部分です。

この記事では、TypeScriptのクラスに関する基本的な使い方から応用的な使い方、さらに注意点やカスタマイズ方法まで、10の実用的なサンプルコードを通じて徹底的に解説します。

初めてTypeScriptのクラスに触れる方でも、しっかりと理解できる内容となっています。

●TypeScriptクラスの基本

○クラスとは?

クラスは、オブジェクト指向プログラミングにおける中核的な概念であり、データとそれを操作するメソッドを1つの単位にまとめたものを指します。

例えば、動物を表現するクラスを考えた場合、動物の属性(名前や種類)と動物の動き(歩く、走る)などの動作をメソッドとして持つことができます。

○TypeScriptでのクラスの特徴

JavaScriptには元々クラスベースのオブジェクト指向は存在しないため、プロトタイプベースの継承を使用してきました。

しかし、TypeScriptでは、より一般的なクラスベースのオブジェクト指向を採用しています。

これにより、JavaやC#などの言語に慣れている開発者でも直感的に理解しやすくなっています。

また、TypeScriptのクラスはアクセス修飾子やジェネリクス、デコレータといった高度な機能もサポートしているため、より柔軟で強力なコードを書くことができます。

●TypeScriptクラスの使い方

TypeScriptのクラス機能は、JavaScriptのクラスを拡張し、静的型付けやアクセス修飾子など、より堅牢なオブジェクト指向プログラミングを可能にします。

ここでは、TypeScriptでのクラスの基本的な使い方に焦点を当てて解説していきます。

○サンプルコード1:基本的なクラスの作成

このコードでは、TypeScriptで簡単なクラスを作成する方法を表しています。

この例では、Personという名前のクラスを定義し、nameというプロパティを持つようにしています。

class Person {
    name: string;

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

    greet() {
        console.log(`こんにちは、${this.name}です!`);
    }
}

const taro = new Person("太郎");
taro.greet();

上記のコードを実行すると、コンソールに「こんにちは、太郎です!」と表示されます。

○サンプルコード2:コンストラクタの利用

このコードでは、TypeScriptのコンストラクタを利用して、オブジェクトの初期化時にプロパティの値を設定する方法を表しています。

この例では、Animalクラスを定義し、speciesというプロパティを持つようにしています。

class Animal {
    species: string;

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

const dog = new Animal("犬");
console.log(dog.species);  // 犬

上記のコードを実行すると、「犬」という文字がコンソールに表示されます。

○サンプルコード3:メソッドの定義と呼び出し

このコードでは、TypeScriptのクラス内でメソッドを定義し、それを呼び出す方法を表しています。

この例では、Calculatorクラスにaddメソッドを追加して、二つの数の合計を計算します。

class Calculator {
    add(x: number, y: number): number {
        return x + y;
    }
}

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

上記のコードを動かすと、計算結果として8という値がコンソールに表示されます。

●TypeScriptクラスの応用例

初心者の方々もTypeScriptのクラスの魅力に気づいてきたのではないでしょうか。

基本的な使い方を理解した後、さらにTypeScriptのクラスの真価を引き出すための応用的なテクニックを見ていきましょう。

○サンプルコード4:継承を使った拡張

このコードではTypeScriptでの継承を使って、基本的なクラスを拡張する方法を表しています。

この例では、動物クラスから犬クラスを継承して新しいメソッドを追加しています。

class 動物 {
    name: string;

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

    移動() {
        console.log(`${this.name}が移動する`);
    }
}

class 犬 extends 動物 {
    // 犬クラス独自のメソッド
    吠える() {
        console.log('ワンワン!');
    }
}

const ポチ = new 犬('ポチ');
ポチ.移動();
ポチ.吠える();

上記のコードを実行すると、ポチという名前の犬が移動し、「ワンワン!」と吠えるという結果を得られます。

○サンプルコード5:アクセス修飾子の活用

このコードではアクセス修飾子を使って、クラスのプロパティやメソッドの可視性を制御する方法を表しています。

この例では、private修飾子を使って外部からアクセスできないプロパティを作成しています。

class 銀行 {
    private 預金額: number;

    constructor(預金: number) {
        this.預金額 = 預金;
    }

    預け入れ(金額: number) {
        this.預金額 += 金額;
        console.log(`預け入れ後の残高: ${this.預金額}`);
    }
}

const yamadaの口座 = new 銀行(5000);
yamadaの口座.預け入れ(2000);

上記のコードを実行すると、「預け入れ後の残高: 7000」という結果が表示されます。

但し、預金額は外部から直接参照や変更ができないため、安全に管理できます。

○サンプルコード6:静的メンバの利用

このコードでは、静的メンバとして変数やメソッドをクラスに紐づける方法を表しています。

この例では、Mathクラスのようにインスタンスを生成せずに使用できるメソッドを定義しています。

class ユーティリティ {
    static PI = 3.141592;

    static 円の面積(半径: number) {
        return this.PI * 半径 * 半径;
    }
}

console.log(ユーティリティ.円の面積(5));

このコードを動かすと、半径5の円の面積が計算され、結果が表示されます。

静的メンバは、そのクラス名を使用して直接アクセスすることができます。

○サンプルコード7:ジェネリクスを取り入れたクラス

TypeScriptはJavaScriptのスーパーセットとして、型安全を提供しています。

そのため、TypeScriptでは「ジェネリクス」という強力な機能を利用することができます。

ジェネリクスを利用すると、型の柔軟性を保ちながらも、コンパイル時に型の安全性を確保することができます。

このコードではジェネリクスを使ってデータを格納するシンプルなクラスを表しています。

この例では、Storageというクラスを作成し、ジェネリクスを使用して任意の型のデータを格納して、取り出す処理を実装しています。

// Storageクラスの定義。Tはジェネリクスの型変数
class Storage<T> {
    private data: T;

    // データをセットするメソッド
    setItem(item: T) {
        this.data = item;
    }

    // データを取得するメソッド
    getItem(): T {
        return this.data;
    }
}

// 文字列を格納するストレージのインスタンスを作成
const stringStorage = new Storage<string>();
stringStorage.setItem("Hello TypeScript");
console.log(stringStorage.getItem());  // Hello TypeScriptが出力される

// 数値を格納するストレージのインスタンスを作成
const numberStorage = new Storage<number>();
numberStorage.setItem(123);
console.log(numberStorage.getItem());  // 123が出力される

上記のStorageクラスは、ジェネリクスTを使用しています。

これにより、stringStorageでは文字列を、numberStorageでは数値を保存することができるようになります。

setItemメソッドで保存したデータは、getItemメソッドで取得することができます。

実際に上記のコードを実行すると、最初に"Hello TypeScript"という文字列が出力され、次に123という数値が出力されます。

このように、ジェネリクスを使用することで、同じロジックを異なる型に適用することが可能となり、コードの再利用性が高まります。

次に、ジェネリクスを用いたクラスのカスタマイズ例を紹介します。

// 複数のデータを持つStorageクラスの定義
class MultiStorage<T> {
    private data: T[] = [];

    // データを追加するメソッド
    addItem(item: T) {
        this.data.push(item);
    }

    // 全てのデータを取得するメソッド
    getAllItems(): T[] {
        return this.data;
    }
}

const multiStringStorage = new MultiStorage<string>();
multiStringStorage.addItem("TypeScript");
multiStringStorage.addItem("JavaScript");
console.log(multiStringStorage.getAllItems());  // ["TypeScript", "JavaScript"]が出力される

MultiStorageクラスでは、T型の配列をデータとして持っています。

このため、複数のデータを追加し、一度に取得することができます。

実行結果として、["TypeScript", "JavaScript"]という配列が出力されます。

○サンプルコード8:インターフェイスとクラスの連携

TypeScriptでのクラスの開発では、しばしば「インターフェイス」という要素を使って設計が行われます。

インターフェイスは、特定のクラスが持つべきメソッドやプロパティの構造を定義するものです。

ここでは、インターフェイスを活用して、TypeScriptでのクラスの設計を行う方法を解説します。

このコードでは、動物の情報を持つインターフェイスと、それを実装するクラスを紹介しています。

この例では、動物の名前と鳴き声を持つインターフェイスを定義し、それを実装する犬のクラスを作成しています。

// 動物の情報を持つインターフェイス
interface Animal {
    name: string;
    makeSound(): string;
}

// 犬クラス: Animalインターフェイスを実装
class Dog implements Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    // 鳴き声を返すメソッド
    makeSound(): string {
        return "ワンワン";
    }
}

上記のサンプルコードでは、Animalというインターフェイスを定義しており、nameプロパティとmakeSoundというメソッドが定義されています。

次に、このインターフェイスを実装するDogクラスを定義しています。

Dogクラスは、Animalインターフェイスの要件を満たすため、nameプロパティとmakeSoundメソッドを持っています。

このコードを実行すると、犬のクラスを生成して、その鳴き声を確認することができます。

例えば、次のような実行が考えられます。

const myDog = new Dog("ポチ");
console.log(`${myDog.name}の鳴き声は${myDog.makeSound()}です。`);

このコードを実行すると、”ポチの鳴き声はワンワンです。”という出力が得られます。

応用例として、異なる動物のクラスも同様にインターフェイスを実装することができます。

例えば、猫のクラスを追加する場合、同じインターフェイスを実装しながら、makeSoundメソッドを”ニャーニャー”という返り値に変更することで、猫の特性を表現できます。

○サンプルコード9:抽象クラスの活用

TypeScriptでは、具体的な実装を持たない抽象クラスを定義することができます。

抽象クラスは、他のクラスが継承するための基底クラスとして使用され、直接インスタンス化することはできません。

また、抽象クラス内で抽象メソッドを定義することも可能です。これは、具体的な実装を持たないメソッドのことを指します。

このコードでは、抽象クラスとして動物を表すAnimalクラスを定義しています。

この例では、Animalクラスは抽象メソッドmakeSoundを持ち、その後のサブクラスでこのメソッドの具体的な実装を提供します。

// 抽象クラスの定義
abstract class Animal {
    abstract makeSound(): void;  // 抽象メソッドの定義

    move(): void {
        console.log('動物が移動します。');  // 通常のメソッドも定義可能
    }
}

// 抽象クラスのサブクラスとして、Dogクラスを定義
class Dog extends Animal {
    makeSound(): void {
        console.log('ワンワン!');
    }
}

// 抽象クラスのサブクラスとして、Catクラスを定義
class Cat extends Animal {
    makeSound(): void {
        console.log('ニャー!');
    }
}

こちらのコードを実行すると、抽象クラスAnimalは直接インスタンス化することはできませんが、サブクラスとして定義したDogクラスやCatクラスはインスタンス化可能です。

それぞれのサブクラスで定義したmakeSoundメソッドを呼び出すことで、異なる動物の鳴き声を出力できます。

例えば、次のようにサブクラスのインスタンスを作成し、メソッドを呼び出すことで動物の鳴き声を確認することができます。

let myDog = new Dog();
myDog.makeSound();  // ワンワン!

let myCat = new Cat();
myCat.makeSound();  // ニャー!

実際に上記のコードを実行すると、「ワンワン!」と「ニャー!」がコンソールに表示されます。

このように抽象クラスは、共通の機能や振る舞いを持つオブジェクトをモデル化する際に非常に役立ちます。

抽象クラスを使用する際の注意点として、継承先のサブクラスで必ず抽象メソッドの実装を提供しなければならない点が挙げられます。

もし、サブクラスで抽象メソッドの実装を提供しない場合、コンパイルエラーとなります。

また、応用例として、抽象クラス内で抽象プロパティも定義することができます。

抽象プロパティは、サブクラスで具体的な値を持つプロパティとして実装される必要があります。

○サンプルコード10:デコレータを使用したクラスの拡張

TypeScriptでは、デコレータを使用してクラス、プロパティ、メソッド、アクセス修飾子、そしてパラメータに特定の動作や情報を追加することができます。

この部分では、デコレータの基本的な使い方と、クラス内での利用例について詳しく解説していきます。

デコレータは、TypeScriptの先進的な機能の一つで、JavaやC#などの他のプログラミング言語でも類似の機能が見られます。

デコレータを用いることで、クラスやそのメンバにメタデータを追加したり、振る舞いを変更したりすることができるのです。

下記のサンプルコードでは、クラスのメソッドに対して、特定の動作を追加するシンプルなデコレータを表しています。

この例では、logMethodというデコレータを作成して、メソッドが呼び出された際にコンソールにログを出力する機能を追加しています。

// メソッドデコレータを定義
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // オリジナルのメソッドを取得
    const originalMethod = descriptor.value;

    // 新しいメソッドの定義
    descriptor.value = function(...args: any[]) {
        // メソッドが呼び出された際の動作を記述
        console.log(`メソッド "${propertyKey}" が呼び出されました。`);

        // オリジナルのメソッドを実行
        return originalMethod.apply(this, args);
    };
    return descriptor;
}

class SampleDecorator {
    @logMethod
    sayHello(name: string) {
        return `Hello, ${name}!`;
    }
}

const sample = new SampleDecorator();
sample.sayHello("TypeScript");

このコードを実行すると、「メソッド “sayHello” が呼び出されました。」というメッセージがコンソールに表示されます。

これにより、sayHelloメソッドの呼び出しを追跡することができます。

デコレータは非常に強力な機能を持っていますが、過度な使用はコードの可読性を低下させる可能性があるため、使用する際は注意が必要です。

デコレータの主な目的は、クラスやそのメンバの振る舞いを変更することではなく、メタデータを追加することにあります。

また、デコレータはカスタマイズが可能であり、さまざまな用途で利用することができます。

下記の例では、クラスのプロパティに初期値を設定するデコレータを作成しています。

// プロパティデコレータを定義
function DefaultValue(value: string) {
    return function(target: any, propertyKey: string) {
        target[propertyKey] = value;
    };
}

class SampleDefault {
    @DefaultValue("Default Name")
    name: string;
}

const sampleDefault = new SampleDefault();
console.log(sampleDefault.name); // "Default Name" と表示される

この例では、@DefaultValueデコレータを用いてnameプロパティに「Default Name」という初期値を設定しています。

●注意点と対処法

TypeScriptを使用してクラスを設計する際、初心者や経験者であっても気を付けるべき注意点や、よくあるトラブルとその対処法をいくつか紹介します。

○クラスのプロパティとメソッドの命名

TypeScriptにおけるクラスの設計では、変数や関数と同様にプロパティやメソッドの命名にも注意が必要です。

名前が長すぎるとコードが読みにくくなる一方、短すぎると意味が伝わりにくくなります。

このコードでは、Userクラスを定義し、その中で短すぎるプロパティ名を使っています。

class User {
    n: string;  // 'name' の略
    a: number;  // 'age' の略

    constructor(n: string, a: number) {
        this.n = n;
        this.a = a;
    }
}

この例では、naというプロパティ名は非常に短く、コードを読む人にとって意味がわかりにくいでしょう。

対処法として、少し長くてもわかりやすい名前を使用することをおすすめします。

class User {
    name: string;
    age: number;

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

○nullとundefinedの取り扱い

TypeScriptのクラスでプロパティを定義する際、初期値を設定しないとundefinedとなります。

さらに、strictNullChecksが有効の場合、nullundefinedを許容しない型にこれらの値を代入しようとするとエラーになります。

このコードでは、Carクラスにおいてbrandプロパティを定義していますが、初期値を設定していません。

class Car {
    brand: string;
}

このような場合、brandundefinedとして扱われ、このプロパティにアクセスすると問題が生じる可能性があります。

対処法として、次のような方法が考えられます。

  1. プロパティに初期値を設定する。
  2. コンストラクタ内で必ず値を設定するようにする。
  3. プロパティの型にnullundefinedを許容するようにする。

上記のCarクラスの場合、次のように修正することで問題を回避できます。

class Car {
    brand: string;

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

○過度なアクセス修飾子の使用

TypeScriptのクラスには、プロパティやメソッドに対してアクセス修飾子(public, private, protectedなど)を設定することができます。

しかし、これらを過度に使用すると、コードの複雑さが増加し、保守性が低下する可能性があります。

例えば、多くのプロパティやメソッドにprivateを設定しすぎると、そのクラスの拡張やテストが難しくなります。

対処法として、次の点に注意すると良いでしょう。

  1. 必要最低限のアクセス修飾子のみを使用する。
  2. サブクラスでの利用を考慮し、protectedを適切に使用する。
  3. テストやデバッグのために、一時的にアクセス修飾子を変更することも考慮する。

こうした注意点と対処法を把握しておくことで、TypeScriptでのクラス設計がよりスムーズに行えるでしょう。

●カスタマイズ方法

TypeScriptのクラスは非常に柔軟で、多彩なカスタマイズ方法があります。

ここでは、TypeScriptのクラスをさらに効果的に活用するためのカスタマイズのテクニックを紹介します。

具体的なサンプルコードとともに、その方法を詳しく解説します。

○ミックスインを用いた機能の追加

このコードでは、TypeScriptでのミックスインを使って、複数のクラスから機能を取り入れる方法を表しています。

この例では、TalkableRunnableという2つのクラスを合成して、新しいクラスRobotを生成しています。

// Talkableクラスの定義
class Talkable {
    talk() {
        console.log("こんにちは!");
    }
}

// Runnableクラスの定義
class Runnable {
    run() {
        console.log("走ります!");
    }
}

// Robotクラスの定義
class Robot extends Talkable {
    // RunnableのメソッドをRobotに追加
    run: Runnable['run'] = new Runnable().run;
}

const robot = new Robot();
robot.talk();  // こんにちは!と表示されます
robot.run();   // 走ります!と表示されます

上記のように、ミックスインを使用することで、複数のクラスのメソッドを一つのクラスに統合することが可能です。

この方法で、コードの再利用性を高めることができます。

○条件付きプロパティの実装

このコードでは、条件に基づいてクラスのプロパティを持つか持たないかを決定する方法を表しています。

この例では、isActiveプロパティがtrueの場合のみ、activeSinceプロパティを持つUserクラスを定義しています。

type ActiveUser = {
    isActive: true;
    activeSince: Date;
}

type InactiveUser = {
    isActive: false;
}

type User = ActiveUser | InactiveUser;

const user1: User = {
    isActive: true,
    activeSince: new Date()
};

const user2: User = {
    isActive: false
};

このように、TypeScriptの型システムを利用して、条件に応じてプロパティを持つか持たないかを動的に制御することができます。

○プライベートフィールドの利用

TypeScript 3.8から、クラスのフィールドに#プレフィックスを付けることで、そのフィールドをプライベートとして宣言することができるようになりました。

このコードでは、#プレフィックスを使用して、passwordフィールドをプライベートフィールドとして定義しているAccountクラスを表しています。

この例では、外部から直接passwordフィールドにアクセスすることはできません。

class Account {
    #password: string;

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

    checkPassword(input: string) {
        return this.#password === input;
    }
}

const myAccount = new Account("secret123");
console.log(myAccount.checkPassword("secret123"));  // trueと表示されます

外部から#passwordに直接アクセスしようとすると、コンパイルエラーが発生します。

これにより、クラスの内部状態を適切に隠蔽することができ、安全性が向上します。

まとめ

TypeScriptのクラスを学ぶという過程は、プログラミングのスキルアップやコードの品質を向上させる鍵となります。

この記事では、TypeScriptのクラスの基本から応用、カスタマイズ方法までを詳細に解説しました。

学び続ける姿勢を持ち、最新の情報や技術をキャッチアップして、自らのスキルセットをブラッシュアップしていくことをおすすめします。

そして、この記事がTypeScriptのクラスを学ぶ一助となれば幸いです。