動作環境
今回もPlaygroundでガシガシ書いていきます。
TypeScript Playground
参考
インターフェースとは?
クラスやオブジェクトの構造を定義し、名前をつけれるものです。
とりあえずインターフェースを使ってみましょう
以下の関数があるとします。
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
{ label: string型の値 }
という形式のオブジェクトを引数として受け取り、キーlabel
に対応する値をconsole.log
で出力するという関数です。
これをinterface
を使って書き換えていきます。
interface LabeledValue { // 1. interface定義
label: string;
}
// 2.関数定義
function printLabel(labeledObj: LabeledValue): void {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
- まず、
interface
を使ってキー名がlabel
で、値がString型であるという構造に名前をつけます。今回は、LabeledValue
という名前で定義しています。 - 関数定義では、先程定義したインターフェース(LabeledValue)を使います。
もし、渡された引数がLabeledValue
の構造にマッチしていなければエラーが発生します。
let erroObj = { name: 'TypeScript' };
printLabel(erroObj);
// => Argument of type '{ name: string; }' is not assignable to parameter of type 'LabeledValue'.
// Property 'label' is missing in type '{ name: string; }' but required in type 'LabeledValue'.
ケースごとのインタフェース利用例
オプショナルなプロパティがある場合
必ず存在するわけではないプロパティを定義したい場合もあるでしょう。
そのようなときには、関数定義のオプショナル引数と同じように?
を使います。
例えば、ユーザのプロフィールとして、ブログ、Twitter、Facebook、InstagramのURLがプロパティとしてあるが、
それぞれは必ず存在するわけではないとします。
このような場合は以下のようなインタフェースとなります。
interface Profile {
nickname: string;
age: number;
blog?: string;
twitter?: string;
facebook?: string;
instagram?: string;
}
Readonlyなプロパティ
オブジェクト生成時のみ値をセットし、変更できないようにしたい場合は、プロパティ名の前にreadonly
をつけます。
interface Profile {
nickname: string;
readonly age: number;
}
let profile: Profile = { nickname: 'tom', age: 20 };
profile.age = 22;
// Cannot assign to 'age' because it is a read-only property.
また、TypeScriptには、Array型から要素を変更するメソッドをすべて除去したReadonlyArray<T>
型もあります。
そのため、ReadonlyArray<T>
型の配列は要素を変更しようとするとエラーになります。
試してみましょう。
let numbers: ReadonlyArray<number> = [1, 20, 3];
numbers[1] = 2; // Index signature in type 'readonly number[]' only permits reading.
numbers.push(4); // Property 'push' does not exist on type 'readonly number[]'.
未定義のプロパティも許可する方法
定義されたプロパティ以外が存在するとエラーになってしまいます。
interface Profile {
nickname: string;
readonly age: number;
}
let profile: Profile = { nickname: 'tom', age: 20, gender: 'male' }; // エラー
// Type '{ nickname: string; age: number; gender: string; }' is not assignable to type 'Profile'.
// Object literal may only specify known properties, and 'gender' does not exist in type 'Profile'.
例えば、nickname
とage
さえあれば、あとは何があっても良いという場合であれば、index signatureが使えます。
interface Profile {
nickname: string;
readonly age: number;
[key: string]: any;
}
let profile: Profile = { nickname: 'tom', age: 20, gender: 'male' };
こうすることで、Profileは「nickname
とage
プロパティがあって、それ以外はどんなプロパティがあってもよい」とすることができます。
インターフェースの継承
インターフェースを継承して、新たにインターフェースを宣言することも可能です。
継承するときは、extends
を使います。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {} as Square;
square.sideLength = 10;
square.color = 'red';
また、カンマ区切りで複数のインターフェースを継承することもできます。
interface Square extends Shape, Rectangle {
...
}
関数型
インターフェースは関数型も表すことができます。
引数とその型、そして戻り値の型を記述します。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
インターフェースとクラス
インターフェースはimplements
キーワードを使って、クラスに特定の構造を強制することもできます。
つまり、インターフェースで宣言したプロパティや関数をクラスが持っていないといけないという制約をつけることができます。
(JavaやC#では最もよく使われる方法らしいです。)
例えば、デジタル時計クラス(DigitalClock
)とアナログ時計クラス(AnalogClock
)があるとして、どちらのクラスにもcurrentTime
プロパティとsetTime
メソッドを実装していてほしい場合、インターフェースをつかうと以下のように書くことができます。
interface ClockInterface {
currentTime: Date;
setTime(date: Date): void;
}
class DigitalClock implements ClockInterface {
currentTime: Date;
constructor() {
this.currentTime = new Date();
}
setTime(date: Date) {
this.currentTime = date;
}
}
class AnalogClock implements ClockInterface {
currentTime: Date;
constructor() {
this.currentTime = new Date();
}
setTime(date: Date) {
this.currentTime = date;
}
}
もし、ClockInterface
を満たさない構造だったら以下のようにエラーになります。
class AnalogClock implements ClockInterface {
constructor() {
}
setTime(date: Date) {
}
}
// Class 'AnalogClock' incorrectly implements interface 'ClockInterface'.
// Property 'currentTime' is missing in type 'AnalogClock' but required in type
// 'ClockInterface'.
これだったら継承でも良くない?と思ってしまいます。
「基底クラスに抽象メソッドを定義して、子クラスでその抽象メソッドをオーバーライドいないといけない」といったように実装すれば同じことができます。
もちろん継承でも良いのですが、TypeScriptは単一継承なので、複数のクラスを継承することができません。しかし、インターフェースの場合は、いくつでもimplements
できるのです。
そのため、目的ごとにいくつかのインターフェースを宣言して、そのうち必要なものをimplements
するといったことができます。
class Watch implements ClockInterface, WatchInterface {
// ...
}
以上、Interfaceについてでした。
次回は、TypeScript 書いて覚えるジェネリック