クラス

イントロダクション

伝統的なJavaScriptでは、再利用可能なコンポーネントを構築する基本的な方法として、 関数とプロトタイプベースの継承に焦点を当てていましたが、 クラスが機能を継承し、それらのクラスからオブジェクトを構築するオブジェクト指向のアプローチに親しんだプログラマーにとっては、 少々扱いづらいものでした。

ECMAScript 6としても知られるECMAScript 2015では、 JavaScriptプログラマーは、このクラスベースのアプローチのオブジェクト指向を使用してアプリケーションを構築出来るようになります。 TypeScriptでは、開発者がこれらの技法を使用できるようにしており、 コンパイルされたJavaScriptは、次世代のJavaScriptのバージョンを待つこと無く、主要な全てのブラウザとプラットフォームで動作します。

クラス

それでは、シンプルなクラスベースの例を見てみましょう。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

C#またはJavaを使用したことがある人であれば、親しみが持てる文法でしょう。 これは、新しいクラスであるGreeterを宣言しています。 このクラスは、greetingというプロパティ、コンストラクタ、greetメソッドの、3つのメンバを持ちます。

このクラスのあるメンバを参照する際に、前にthis.を付けていることにお気づきかと思います。 これは、メンバへのアクセスを示すものです。

最後の行では、newを使用してGreeterのインスタンスを構築しています。 これは事前に定義しているconstructorを呼び出し、 Greeterを形づくる新しいオブジェクトを作成して、初期化処理を実行します。

継承

TypeScriptでは、一般的なオブジェクト指向パターンを使用することができます。 そして、当然クラスベースのプラグラミングの最も基本的なパターンの1つである「継承」を使用して、 既存のクラスから新しいクラスを作成することが可能です。

次の例を確認してみましょう。

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

この例で、TypeScriptの継承機能のほとんどが表されており、これは他の言語でも共通するものです。 サブクラスを作成するのに、extendsキーワードが使用されていることが確認できます。 また、サブクラスであるHorseSnakeAnimalクラスをベースにしており、 その機能にアクセスできることも確認することができます。

サブクラスは、constructor関数を持つのであれば、その中でsuper()を呼びださなければいけません。 これはベースクラスのconstructor関数を実行します。

また、この例はベースクラスのメソッドをサブクラス用にするために、どのように上書きするかを示しています。 ここではSnakeHorseのどちらもAnimalからのmoveメソッドを上書きして、 それぞれの固有の機能を与えています。 トム(tom)をAnimalとして宣言しても、その値はHorseであるため、 tom.move(34)の呼び出しは、Horseの上書きメソッドを呼び出します。

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

public、private、protected

public(デフォルト)

我々の例では、プログラムを通して、宣言したメンバに自由にアクセスすることが出来ました。 あなたが他の言語のクラスに慣れ親しんでいるのであれば、 あなたは上記の例で、これを行うためにpublicを必要としていないことに気づいたかもしれません。 例えば、C#であればpublicを各メンバに対して、明示的にラベル付けをしなければいけません。 TypeScriptでは、デフォルトで各メンバはpublicになります。

publicを明示的に指定することも可能です。 前のセクションのAnimalクラスを、次のように書くことも可能です。

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

private

メンバにprivateを指定した場合、クラス外からのアクセスは不可になります。 下記はその例になります。

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error: 'name' is private;

TypeScriptは構造型(structural type)のシステムです。 異なる2つの型を比較する場合、それがどこから生じたものかに関わらず、 全てのメンバの型に互換性があれば、それらは互換性があると判断されます。

ただし、privateprotectedメンバを持つ型を比較する際には、 これらの型は異なるものとして扱われます。 2つの型の互換性を考慮する場合、一方がprivateのメンバを持つのであれば、 もう一方も同じ宣言から生じたprivateメンバを持たなければいけません。 protectdメンバにも同様のことが言えます。

実演を通して、サンプルがどのようにして改善されるのかを確認してみましょう。

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

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

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

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // Error: 'Animal' and 'Employee' are not compatible

この例でAnimalRhinoを持ち、 RhinoAnimalのサブクラスになっています。 また、新しいクラスであるEmployeeも持ち、 形状(メンバの名前や型)の面ではAnimalと一致しています。 これらのクラスのインスタンスを作成し、それぞれを割り当ててみると何が起こるのかを見てみましょう。

AnimalRhinoは、Animalのprivate name: stringの同じ宣言から同じメンバを共有するため、 互換性があるとみなされます。 しかし、Employeeにはこれが当てはまりません。 AnimalEmployeeを割り当てようとすると、型に互換性が無いとしてエラーになります。 Employeeもまたnameというprivateメンバを持っているのにも関わらず、 宣言したAnimalの1つとはみなされません。

protected

protected修飾子は、指定されたメンバが継承先のクラスのインスタンスからでもアクセス可能であることを除いて、 ほとんどprivate修飾子のように振る舞います。 下記はその一例になります。

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

class Employee extends Person {
    private department: string;

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

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error

Personの外部からnameを使用できない一方、 EmployeePersonを継承していることから、 Employeeのインスタンスのメソッドから、それを使用できることに注目してください。

コンストラクタにも、protectedを指定することができます。 これは、そのクラス自身が含まれるクラスの外側でインスタンス化することはできませんが、拡張は可能であることを意味します。 下記はその例になります。

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee can extend Person
class Employee extends Person {
    private department: string;

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

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // Error: The 'Person' constructor is protected

引数プロパティ

先の例では、privateメンバであるnameconstructorの引数theNameを宣言し、 即座にnametheNameを設定しています。 これは非常によく見られる慣習です。 引数プロパティは、作成・初期化を一箇所で済ませることを可能にしてくれます。 下記は先程のAnimalクラスを、引数プロパティを使用したバージョンになります。

class Animal {
    constructor(private name: string) { }
    move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

theNameを完全に無くし、private name: stringだけを使用し、 constructornameメンバの作成と初期化を行っています。 宣言と割り当てが、1つの場所に統合されています。

引数プロパティは、アクセシビリティ修飾子をconstructorの引数の前に付けることで宣言されます。 privateを引数プロパティに使用することでprivateなメンバの宣言と初期化が行われ、 publicprotectedも同じようにこれが行われます。

アクセサ

TypeScriptは、オブジェクトのメンバへのアクセスの方法のひとつとしてgetter/setterをサポートしてます。 これは、あなたのメンバーの各オブジェクトにアクセスする際に、きめ細やかな制御を持たせることを可能にしてくれます。

単純なクラスをgetsetを使用するものに変換してみましょう。 まず、getter/setterの無い例を元に始めてみましょう。

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

開発者が無造作に直接fullNameを設定できるようにするのは非常に手軽ではありますが、 ふとしたことで誰かがそれを変更すると、予期しない問題が発生することになりかねません。

この例では、ユーザーのemployeeの変更が許可される前に、 ユーザーが秘密のパスコードが利用可能であることをチェックします。 fullNameの直接のアクセスを、setを使用したパスコードをチェックする動作に置き換えることで、 これを実現します。 これに応じて、これまでと同様に動作し続けるようにgetも追加します。

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

アクセサがパスコード(passcode)をチェックしているかを確認するために、 パスコード(passcode)を別のものに変更することで、これが一致しない場合に警告メッセージが出力され、 employeeを更新するためのアクセスが出来ないことが確認できます。

アクセサに関する注意点

まず、アクセサを使用するには、コンパイラがECMAScript 5、またはそれ以上のバージョンを出力することが求められます。 ECMAScript 3へのサポートはされません。

次に、getが有りsetの無いアクセサは、自動的にreadonlyであると推論されます。 これは、あなたのプロパティを使用するユーザーがそれを変更できないことが分かるため、 コードから.d.tsファイルを生成するときに役立ちます。

静的プロパティ

ここまでで、インスタンス化された際にオブジェクト上に現れる、 クラスのインスタンスメンバについてのみ話してきました。 インスタンスではなくクラス上に現れる、クラスの静的(static)メンバを作成することも可能です。

この例では、originに対してstaticを使用することで全てのgridに対しての総体的な値としています。 各インスタンスは、クラスの名前を値の前に指定することで、この値にアクセスします。 インスタンスのアクセスするものの前にthis.を指定したのと同様に、 ここでは静的アクセスするものの前にGrid.を指定します。

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

抽象クラス

抽象(Abstract)クラスは、それを継承する他のクラスのために用意される基底クラスです。 抽象クラスは直接インスタンス化してはいけません。 インターフェースとは異なり、抽象クラスはメンバへの詳細な実装を含めることも可能です。 abstractキーワードは抽象クラスだけでなく、抽象クラス内の抽象メソッドの定義にも使用されます。

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

抽象クラス内に抽象メソッドとして指定されたメソッドには実装は含まれず、 継承した先のクラスで実装しなければいけません。 抽象メソッドはインターフェースのメソッドと同様の文法を共有します。 どちらの定義も、メソッドの本文(body)を含まないという特徴があります。 ただし、抽象メソッドはabstractキーワードを付ける必要があり、 任意でアクセス修飾子を指定することが可能です。

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log("Department name: " + this.name);
    }

    abstract printMeeting(): void; // 継承するクラスで実装しなければいけない
}

class AccountingDepartment extends Department {

    constructor() {
        super("Accounting and Auditing"); // 継承したクラスのコンストラクタ内でsuper()を呼ばなけれいけない
    }

    printMeeting(): void {
        console.log("The Accounting Department meets each Monday at 10am.");
    }

    generateReports(): void {
        console.log("Generating accounting reports...");
    }
}

let department: Department; // OK: 抽象型を参照する変数の作成
department = new Department(); // エラー: 抽象クラスのインスタンスの作成は不可
department = new AccountingDepartment(); // OK: 抽象クラスでは無いサブクラスの作成と割り当て
department.printName();
department.printMeeting();
department.generateReports(); // エラー: このメソッドは定義した抽象型上には存在しない

上級テクニック

コンストラクタ関数

TypeScriptでクラスを宣言する時、実際には同時に複数の宣言を作成しています。 その1つはクラスのインスタンスの型です。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

ここで、let greeter: Greeterとした際に、 Greeterクラスのインスタンスの型としてGreeterを使用しています。 これは、他のオブジェクト指向言語から深く身についた習慣と言えるでしょう。

また、他に作成されているものに、我々がコンストラクタ関数と呼ぶものがあります。 これは、クラスのインスタンスがnewされた際に呼び出される関数です。 ここで実際にどのようなことが行われているのかを、上記の例から出力されるJavaScriptで確認してみましょう。

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

ここでは、let Greeterはコンストラクタ関数に割り当てられています。 newを呼び出してこの関数を実行すると、クラスのインスタンスを取得します。 また、コンストラクタ関数はクラスの全ての静的メンバも含みます。 各クラスに対して違った見方をするのであれば、インスタンス側と静的側が存在するということが挙げられます。

この違いを見るために、例を少し編集してみましょう。

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

この例では、greeter1は以前と同様に動作します。 Greeterクラスをインスタンス化して、このオブジェクトを使用します。 これは、既に見てきました。

次にクラスを直接使用しています。 ここでは、greeterMakerという新しい変数を作成しています。 この変数は自身のクラスを、別の言い方をすればコンストラクタ関数を保持します。

ここでtypeof Greeterを使用し、インスタンスの型では無く、「Greeterクラス自身の型を渡してください」としています。 より正確に言うと、「Greeterと呼ばれるシンボルの型をください」となり、これはコンストラクタ関数の型になります。

この型は、Greeterクラスのインスタンスを作成するコンストラクタと併せて、 Greeterの全ての静的メンバを含みます。 我々はgreeterMakernewを使用して、 Greeterの新しいインスタンスを作成して、従来通りに実行することを示しています。

インターフェースとしてクラスを使用

前回のセクションで話したように、クラス宣言はクラスのインスタンスを表す型とコンストラクタ関数の、2つのものを作成します。 クラスが型を作成するため、同じ場所でインターフェースに用いてそれを使用することが可能です。

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

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

 Back to top

© https://github.com/Microsoft/TypeScript-Handbook

このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • ドキュメントの情報が古い可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。