TypeScript Handbook を読み進めていく第四回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes (今ココ)
- Functions
- Generics
- Enums
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules
- Namespaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
Classes
Classes
まずは簡単な例から見ていきましょう。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
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 では一般的なオブジェクト指向プログラミングも可能です。
クラスベースプログラミングのもっとも基礎的なパターンのひとつが、継承を使用した既存クラスの拡張です。
例を見てみましょう。
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();
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
基底 クラスから 派生 しており、派生元先のクラスのことをスーパークラス、サブクラスと呼びます。
Dog
は Animal
の機能を拡張しているため、Dog
クラスのインスタンスを作成して bark()
と move()
を呼び出すことができます。
続いて、より複雑な例を見てみましょう。
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);
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
クラスから Horse
、Snake
クラスを作成しています。
上記の例との違いは、派生クラスがともにコンストラクタを持っており、基底クラスのコンストラクタを呼び出すために super()
を 呼び出さなければならない ことです。
もっと言うと、コンストラクタ内でプロパティにアクセスする前には 常に super()
を呼び出す必要があります。
これは重要なルールであり、TypeScript ではこれを強制しています。
super()
メソッドの呼び出しを忘れてもコンパイルエラーになるので大丈夫
さらに、この例では親クラスのメソッドをサブクラスでオーバーライドする方法についても示しています。
この時、仮に tom
が Animal
クラスとして宣言されていたとしても、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
キーワードを付与しても問題ありません。
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
キーワードを付与すると、そのクラス外からアクセスできなくなります。
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // エラー。'name' は private なのでアクセスできない
var Animal = (function () {
function Animal(theName) {
this.name = theName;
}
return Animal;
}());
new Animal("Cat").name; // エラー。'name' は private なのでアクセスできない
private
メンバとして宣言しても JavaScript のコードが変わるわけでもなく、コンパイラでのチェックが有効になるだけなのね
TypeScript では 2 つの型が等しいか判断する時に、それらがどこで宣言されたかによらず、すべてのメンバに互換性があるかどうかだけを基に判断します。
しかし、クラスが private
または protected
メンバを持つ場合、それらのメンバが同じクラスで宣言されたものの場合のみ、型が等しいとみなします。
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
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() {
// ここからは '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
として宣言することもできます。
その場合、インスタンス化することができず、継承のみが可能なクラスとなります。
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
キーワードをつけることでプロパティを読み取り専用にすることが可能です。
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
コンストラクタで受け取った引数を基にプロパティを初期化するだけであれば、パラメータプロパティ を使用することで一度に宣言と初期化を行うことができます。
パラメータプロパティを宣言するには、コンストラクタの引数にアクセス修飾子 (private
、protected
、public
) や readonly
を付与します。
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
var Octopus = (function () {
function Octopus(name) {
this.name = name;
this.numberOfLegs = 8;
}
return Octopus;
}());
便利は便利だけど、ぱっと見でプロパティの一覧が分かりにくくなるのはデメリットかなぁ
Accessors
TypeScript では getter/setter を宣言することで、プロパティへのアクセスを捕捉することができる他、より細かくプロパティへのアクセスを制御することが可能です。
以下の getter/setter を使用していない例では、fullName
を好きなように変更することができてしまいます。
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
以下の例では setter を宣言して fullName
の設定時に追加のチェックを行うようにしています。
また、通常通り fullName
にアクセスできるよう、getter も宣言しています。
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);
}
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
を前に付ける必要があったように、静的メンバにアクセスする場合にはクラス名を前に付ける必要がある点に注意してください。
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}));
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
キーワードを付与する必要がある点が異なります。
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(); // エラー。抽象クラスで宣言されていないメンバにはアクセスできない
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
を型名として使用しています。
これは大半のオブジェクト指向プログラマにとって馴染みのある方法でしょう。
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
にコンストラクタ関数を代入して使用しています。
この時、コンストラクタにはすべての静的メンバが含まれています。
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());
これについてもう少し詳しく説明するために、例を少し修正してみましょう。
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
クラスをインタフェースとして使用することもできます。
class Point {
x: number;
y: number;
}
interface Point3d extends Point { // クラスを使ってインタフェースを拡張している!
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};