LoginSignup
3
4

More than 3 years have passed since last update.

【書いて覚えるTypeScript 】インターフェース(interface)編

Last updated at Posted at 2019-09-29

動作環境

今回もPlaygroundでガシガシ書いていきます。
TypeScript Playground

参考

TypeScript Handbook

インターフェースとは?

クラスやオブジェクトの構造を定義し、名前をつけれるものです。

とりあえずインターフェースを使ってみましょう

以下の関数があるとします。

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);
  1. まず、interfaceを使ってキー名がlabelで、値がString型であるという構造に名前をつけます。今回は、LabeledValueという名前で定義しています。
  2. 関数定義では、先程定義したインターフェース(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'.

例えば、nicknameageさえあれば、あとは何があっても良いという場合であれば、index signatureが使えます。

interface Profile {
    nickname: string;
    readonly age: number;
    [key: string]: any;
}

let profile: Profile = { nickname: 'tom', age: 20, gender: 'male' };

こうすることで、Profileは「nicknameageプロパティがあって、それ以外はどんなプロパティがあってもよい」とすることができます。

インターフェースの継承

インターフェースを継承して、新たにインターフェースを宣言することも可能です。
継承するときは、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 書いて覚えるジェネリック

3
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
3
4