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
Interfaces
Our First Interface
インタフェースの使われ方の一番単純な例は以下の通りです。
コンパイラは printLabel
メソッドの引数に文字列型の label
プロパティが含まれていることをチェックします。
一方で、それ以外のプロパティがあったとしてもコンパイラは何もチェックしません。
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
function printLabel(labelledObj) {
console.log(labelledObj.label);
}
var myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
それでは実際にインタフェースを使ってみましょう。
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
function printLabel(labelledObj) {
console.log(labelledObj.label);
}
var myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
インタフェースはチェックにだけ使われて、コンパイルしたら消えるのね
コンパイラはプロパティがどのような順番になっていても気にしないことを覚えておいてください。
Optional Properties
必ずしもすべてのプロパティが必須とは限らないため、そのようなプロパティを表すにはプロパティ名の末尾に ?
を付けてください。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
function createSquare(config) {
var newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
var mySquare = createSquare({ color: "black" });
任意のプロパティを指定しても JavaScript のコードは特に変わらないか
任意のプロパティの利点は (プロパティ名の打ち間違い等で) インタフェースに定義されていないプロパティを誤って使用しないようチェックできる点です。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.color) {
// エラー。'clor' プロパティは 'SquareConfig' に存在しない
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
Readonly properties
読み取り専用にしたいプロパティはプロパティ名の前に readonly
を付けてください。
interface Point {
readonly x: number;
readonly y: number;
}
この場合、オブジェクトリテラルで初期化することはできますが、変更することはできません。
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // エラー!
TypeScript では ReadonlyArray<T>
クラスを用意しています。
これは Array<T>
に似ていますが、オブジェクトを変更可能なメソッドが削除されています。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // エラー!
ro.push(5); // エラー!
ro.length = 100; // エラー!
a = ro; // エラー!
最後の例は ReadonlyArray
オブジェクトを普通の配列に代入できないことを表していますが、キャストを使用すれば可能です。
a = ro as number[];
readonly
vs const
readonly
と const
の使い分けは簡単です。
対象が変数であれば const
を、プロパティであれば readonly
を使ってください。
Excess Property Checks
インタフェースと任意のプロパティを組み合わせると、次のような落とし穴にハマることがあります。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 }); // おっと、'color' のつもりが 'colour' としてしまった
'color'
プロパティは必須ではないため、このままでは 'width'
プロパティだけが使用されてしまいます。
これを防ぐために TypeScript ではオブジェクトリテラルに対して特別なチェックを行うようにしています。
具体的には、他の変数への代入時やメソッドの引数として渡された時に "対象の型" に含まれないプロパティが存在するとエラーとして扱います。
// エラー。'colour' は 'SquareConfig' に含まれない
let mySquare = createSquare({ colour: "red", width: 100 });
このチェックをバイパスするには単にキャストするだけです。
// 'SquareConfig' に 'opacity' は含まれないけど問題ない
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
もっと良いやり方は余分なプロパティを受け取るために文字列型のインデックスシグネチャを追加することです。
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
'propName'
と指定しているものの、別に引数.propName
で余分なプロパティにアクセスできるわけではない点に注意。
インデックスシグネチャについては後述。
最後に紹介する方法は一度変数に代入することです。
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
単純なコードではそうは思わないかもしれませんが、コードがより複雑になってくると、これらのテクニックを使う必要が出てくるかもしれません。
ただし、多くの場合では、余分なプロパティの存在はバグであることを意識しておいてください。
Function Types
インタフェースは特定のプロパティを持つオブジェクトを表す以外に、メソッドの型を表すこともできます。
これには引数と戻り値だけを持つ関数宣言のように表現します。
この時、引数リストには名前と型が必須です。
interface SearchFunc {
(source: string, subString: string): boolean;
}
後は他のインタフェースと同じように使用するだけです。
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
この時、引数の名前は必ずしも一致する必要はありません。
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
型を指定していなくても、TypeScript は変数の型を元に型推論を行います。
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
とはいえ、なるべく型は指定した方がいいと思う
Indexable Types
TypeScript では a[10]
や ageMap["daniel"]
のように、インデックスでアクセスできる型も表現できます。
インデックス可能な型は、インデックスとして指定可能な型とその戻り値の型を指定した インデックスシグネチャ を持ちます。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
インデックスシグネチャは 文字列型 と 数値型 の 2 種類をサポートしています。
両方の型をサポートすることも可能ですが、その場合、数値型のインデックスシグネチャの戻り値の型は、文字列型のインデックスシグネチャの戻り値の型のサブクラスである必要があります。
なぜなら、JavaScript では数値型のインデックスは文字列に変換されてから使用されるためです。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// エラー。文字列型でアクセスすると Animal が返ってくることがある。
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
それ以外に、インデックスシグネチャはすべてのプロパティの型を統一させる効果もあります。
interface NumberDictionary {
[index: string]: number;
length: number; // OK。length は数値型
name: string; // エラー。name は数値型またはそのサブクラスではない
}
最後に、インデックスシグネチャに readonly
を指定することで、インデックスを指定した代入を禁止することもできます。
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // エラー!
Class Types
Implementing an interface
インタフェースのもっとも一般的な用途はクラス宣言に使用することです。
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
同様にクラスで定義すべきメソッドを宣言することもできます。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
Difference between the static and instance sides of classes
クラスとインタフェースを使用するにあたって、クラスは静的メンバとインスタンスメンバという 2 種類のメンバを持つ点を意識しておいてください。
例えば、以下の例はエラーとなります。
なぜなら、インタフェースを実装する際にはインスタンスメンバしかチェックされないため、静的メンバであるコンストラクタがチェックの対象にならないからです。
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
コンストラクタ(コンストラクタシグネチャ)を持つインタフェースは実装(継承)できないということかな?
代わりに静的メンバを直接使用する必要があります。
以下の例ではコンストラクタ用のインタフェース ('ClockConstructor'
) とメソッド用のインタフェース ('ClockInterface'
) をそれぞれ用意しています。
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Extending Interfaces
インタフェースはクラスのように拡張することもできます。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
また、複数のインタフェースを基に拡張することも可能です。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
Hybrid Types
インタフェースでは、これまでに挙げた機能を組み合わせて使用することが可能です。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { }; // まだインタフェースをすべて実装していないため、'Counter' 型として宣言していない (できない)
counter.interval = 123;
counter.reset = function () { };
return counter; // 'Counter' 型のすべてのプロパティ/メソッドを用意したため、'Counter' 型として返却できる
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
Interfaces Extending Classes
クラスを拡張してインタフェースを作成した場合、private/protected メンバも含めてクラスのメンバはすべて継承されますが、その実装は引き継がれません。
つまり、private/protected メンバを持つクラスを継承したインタフェースは、そのクラス自身、またはそのサブクラスでしか実装することはできないということです。
このことは特定のプロパティを持つサブクラスに対してのみ、動作するコードを作成したい場合に役立ちます。
この時、サブクラスで必要なことは基底クラスを継承することだけです。
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
}
// エラー: 'state' プロパティが 'Image' クラスに存在しない
class Image implements SelectableControl {
select() { }
}
class Location {
}
例えば、上記の例では SelectableControl
クラスは private な state
プロパティを含む、Control
クラスのすべてのメンバを含んでいます。
state
プロパティは private メンバのため、SelectableControl
を実装できるのは Control
の子孫クラスのみとなります。
Control
クラス内では SelectableControl
のインスタンスを介して state
メンバにアクセスすることができます。
そのため、SelectableControl
は select
メソッドを持つ Control
クラスのように振る舞います。
Button
、TextBox
クラスは ('Control'
クラスを継承し、かつ、select
メソッドを持っているため) SelectableControl
のサブクラスですが、Image
、Location
クラスは違います。
TextBox
はselect
メソッドを持たないから、SelectableControl
ではないような気が...