LoginSignup
6
4

More than 5 years have passed since last update.

TypeScript Handbook を読む (4. Classes)

Last updated at Posted at 2017-03-11

TypeScript Handbook を読み進めていく第四回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes (今ココ)
  5. Functions
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namespaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Classes

原文

Classes

まずは簡単な例から見ていきましょう。

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

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

var greeter = new Greeter("world");

この例では Greater クラスを宣言しており、メンバとして greeting プロパティ、コンストラクタ、greet メソッドを持っています。

greet メソッドの中で this を使用している点に注目してください。
このようにすることで、他のメンバにアクセスすることができます。

最後の行では new を使ってインスタンスを作成しています。
こうすると、事前に定義したコンストラクタが呼ばれてオブジェクトが初期化されます。

Inheritance

TypeScript では一般的なオブジェクト指向プログラミングも可能です。
クラスベースプログラミングのもっとも基礎的なパターンのひとつが、継承を使用した既存クラスの拡張です。

例を見てみましょう。

TypeScript
class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
JavaScript
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Animal = /** @class */ (function () {
    function Animal() {
    }
    Animal.prototype.move = function (distanceInMeters) {
        if (distanceInMeters === void 0) { distanceInMeters = 0; }
        console.log("Animal moved " + distanceInMeters + "m.");
    };
    return Animal;
}());
var Dog = /** @class */ (function (_super) {
    __extends(Dog, _super);
    function Dog() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    Dog.prototype.bark = function () {
        console.log('Woof! Woof!');
    };
    return Dog;
}(Animal));
var dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

継承を実現するためにこれだけのコードが必要と考えると、altJS 万歳と言わざるをえない

この例では一番基本的な継承の機能である、基底クラスからのプロパティ、メソッドの継承を行っています。
Dog クラスは extends キーワードを使用して Animal 基底 クラスから 派生 しており、派生元先のクラスのことをスーパークラス、サブクラスと呼びます。

DogAnimal の機能を拡張しているため、Dog クラスのインスタンスを作成して bark()move() を呼び出すことができます。

続いて、より複雑な例を見てみましょう。

TypeScript
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);
JavaScript
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();

var Animal = (function () {
    function Animal(theName) {
        this.name = theName;
    }
    Animal.prototype.move = function (distanceInMeters) {
        if (distanceInMeters === void 0) { distanceInMeters = 0; }
        console.log(this.name + " moved " + distanceInMeters + "m.");
    };
    return Animal;
}());

var Snake = (function (_super) {
    __extends(Snake, _super);
    function Snake(name) {
        return _super.call(this, name) || this;
    }
    Snake.prototype.move = function (distanceInMeters) {
        if (distanceInMeters === void 0) { distanceInMeters = 5; }
        console.log("Slithering...");
        _super.prototype.move.call(this, distanceInMeters);
    };
    return Snake;
}(Animal));

var Horse = (function (_super) {
    __extends(Horse, _super);
    function Horse(name) {
        return _super.call(this, name) || this;
    }
    Horse.prototype.move = function (distanceInMeters) {
        if (distanceInMeters === void 0) { distanceInMeters = 45; }
        console.log("Galloping...");
        _super.prototype.move.call(this, distanceInMeters);
    };
    return Horse;
}(Animal));

var sam = new Snake("Sammy the Python");
var tom = new Horse("Tommy the Palomino");

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

この例は上記の例では述べていない、別の機能について説明しています。
先ほどと同じく、extends キーワードを使用して Animal クラスから HorseSnake クラスを作成しています。

上記の例との違いは、派生クラスがともにコンストラクタを持っており、基底クラスのコンストラクタを呼び出すために super()呼び出さなければならない ことです。
もっと言うと、コンストラクタ内でプロパティにアクセスする前には 常に super() を呼び出す必要があります。
これは重要なルールであり、TypeScript ではこれを強制しています。

super() メソッドの呼び出しを忘れてもコンパイルエラーになるので大丈夫

さらに、この例では親クラスのメソッドをサブクラスでオーバーライドする方法についても示しています。
この時、仮に tomAnimal クラスとして宣言されていたとしても、tom.move(34) を実行すると Horse クラスの move() が呼び出されます。

Java とかを使ってるとこれはお馴染みね

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

Public, private, and protected modifiers

Public by default

TypeScript はデフォルトですべてのメンバが public です。
もちろん、明示的に public キーワードを付与しても問題ありません。

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

Understanding private

メンバに private キーワードを付与すると、そのクラス外からアクセスできなくなります。

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

new Animal("Cat").name; // エラー。'name' は private なのでアクセスできない
JavaScript
var Animal = (function () {
    function Animal(theName) {
        this.name = theName;
    }
    return Animal;
}());

new Animal("Cat").name; // エラー。'name' は private なのでアクセスできない

private メンバとして宣言しても JavaScript のコードが変わるわけでもなく、コンパイラでのチェックが有効になるだけなのね

TypeScript では 2 つの型が等しいか判断する時に、それらがどこで宣言されたかによらず、すべてのメンバに互換性があるかどうかだけを基に判断します。
しかし、クラスが private または protected メンバを持つ場合、それらのメンバが同じクラスで宣言されたものの場合のみ、型が等しいとみなします。

TypeScript
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; // OK。'Animal' と 'Rhino' の 'name' プロパティは両方とも 'Animal' クラスで宣言されている
animal = employee; // エラー。'Employee' の 'name' プロパティは 'Animal' クラスで宣言されたものではない

Understanding protected

protectedprivate と似ていますが、宣言したクラスとそのサブクラスからのみアクセスできる点が異なります。

TypeScript
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() {
        // ここからは 'name' にアクセスできる
        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); // エラー。ここからは 'name' にアクセスできない

コンストラクタを protected として宣言することもできます。
その場合、インスタンス化することができず、継承のみが可能なクラスとなります。

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

// 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"); // エラー。'Person'  クラスのコンストラクタは protected

Readonly modifier

readonly キーワードをつけることでプロパティを読み取り専用にすることが可能です。

TypeScript
class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // エラー。'name' は読み取り専用

Parameter properties

コンストラクタで受け取った引数を基にプロパティを初期化するだけであれば、パラメータプロパティ を使用することで一度に宣言と初期化を行うことができます。
パラメータプロパティを宣言するには、コンストラクタの引数にアクセス修飾子 (privateprotectedpublic) や readonly を付与します。

TypeScript
class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}
JavaScript
var Octopus = (function () {
    function Octopus(name) {
        this.name = name;
        this.numberOfLegs = 8;
    }
    return Octopus;
}());

便利は便利だけど、ぱっと見でプロパティの一覧が分かりにくくなるのはデメリットかなぁ

Accessors

TypeScript では getter/setter を宣言することで、プロパティへのアクセスを捕捉することができる他、より細かくプロパティへのアクセスを制御することが可能です。

以下の getter/setter を使用していない例では、fullName を好きなように変更することができてしまいます。

TypeScript
class Employee {
    fullName: string;
}

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

以下の例では setter を宣言して fullName の設定時に追加のチェックを行うようにしています。
また、通常通り fullName にアクセスできるよう、getter も宣言しています。

TypeScript
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);
}
JavaScript
var passcode = "secret passcode";

var Employee = (function () {
    function Employee() {
    }
    Object.defineProperty(Employee.prototype, "fullName", {
        get: function () {
            return this._fullName;
        },
        set: function (newName) {
            if (passcode && passcode == "secret passcode") {
                this._fullName = newName;
            }
            else {
                console.log("Error: Unauthorized update of employee!");
            }
        },
        enumerable: true,
        configurable: true
    });
    return Employee;
}());

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

JavaScript ではこうやって getter/setter を実現するのかー & TypeScript 使うと宣言が楽すぎる…

アクセッサを宣言する場合、ECMAScript 5 以上のコードを生成するようにコンパイラを設定する必要があります。
また、get のみを宣言して set を宣言しなかった場合、自動的に readonly なプロパティとして扱われます。

Static Properties

static キーワードを使用することで 静的 メンバを宣言することが可能です。
インスタンスメンバにアクセスする場合に this を前に付ける必要があったように、静的メンバにアクセスする場合にはクラス名を前に付ける必要がある点に注意してください。

TypeScript
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}));
JavaScript
var Grid = (function () {
    function Grid(scale) {
        this.scale = scale;
    }
    Grid.prototype.calculateDistanceFromOrigin = function (point) {
        var xDist = (point.x - Grid.origin.x);
        var yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    };
    return Grid;
}());
Grid.origin = { x: 0, y: 0 };

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

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

Abstract Classes

abstract キーワードを使用することで、抽象メソッドを持つ抽象クラスを宣言することができます。
抽象メソッドはインタフェースのメソッド宣言と似ていますが、abstract キーワードを付与する必要がある点が異なります。

TypeScript
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; // 抽象クラスの変数を宣言することは可能
department = new Department(); // エラー。抽象クラスをインスタンス化することはできない
department = new AccountingDepartment(); // 抽象クラスでないサブクラスをインスタンス化し、代入することは可能
department.printName();
department.printMeeting();
department.generateReports(); // エラー。抽象クラスで宣言されていないメンバにはアクセスできない
JavaScript
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();

var Department = (function () {
    function Department(name) {
        this.name = name;
    }
    Department.prototype.printName = function () {
        console.log("Department name: " + this.name);
    };
    return Department;
}());

var AccountingDepartment = (function (_super) {
    __extends(AccountingDepartment, _super);
    function AccountingDepartment() {
        return _super.call(this, "Accounting and Auditing") || this;
    }
    AccountingDepartment.prototype.printMeeting = function () {
        console.log("The Accounting Department meets each Monday at 10am.");
    };
    AccountingDepartment.prototype.generateReports = function () {
        console.log("Generating accounting reports...");
    };
    return AccountingDepartment;
}(Department));

var department; // 抽象クラスの変数を宣言することは可能
department = new Department(); // エラー。抽象クラスをインスタンス化することはできない
department = new AccountingDepartment(); // 抽象クラスでないサブクラスをインスタンス化し、代入することは可能
department.printName();
department.printMeeting();
department.generateReports(); // エラー。抽象クラスで宣言されていないメンバにはアクセスできない

Advanced Techniques

Constructor functions

TypeScrpit でクラスを宣言するにあたって、複数の方法で宣言することができます。

1 つ目の方法は以下の通りで、greeter の宣言の時に Greeter を型名として使用しています。
これは大半のオブジェクト指向プログラマにとって馴染みのある方法でしょう。

TypeScript
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());

2 つ目の方法は コンストラクタ関数 と呼ばれる方法で、以下の例では let Greeter にコンストラクタ関数を代入して使用しています。
この時、コンストラクタにはすべての静的メンバが含まれています。

TypeScript
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());

これについてもう少し詳しく説明するために、例を少し修正してみましょう。

TypeScript
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()); // "Hello, there"

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

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet()); // "Hey there!"

greeter1 は最初の例と同じように動作します。
次に、typeof Greeter を使用して greeterMaker 変数に Greeter クラスそのもの (コンストラクタ関数) を代入しています。
これには Greeter のすべての静的メンバと、インスタンスを作成するためのコンストラクタ関数が含まれています。

個人的には 1 番目の宣言の方が自明なので、なるべくこちらを使う方が良い気がしてるけど、2 番目の宣言方法はどういう時に使うべきなんだろう?

Using a class as an interface

クラスをインタフェースとして使用することもできます。

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

interface Point3d extends Point { // クラスを使ってインタフェースを拡張している!
    z: number;
}

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4