21
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScript Handbook を読む (3. Interfaces)

Last updated at Posted at 2017-03-04

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

Interfaces

原文

Our First Interface

インタフェースの使われ方の一番単純な例は以下の通りです。

コンパイラは printLabel メソッドの引数に文字列型の label プロパティが含まれていることをチェックします。
一方で、それ以外のプロパティがあったとしてもコンパイラは何もチェックしません。

TypeScript
function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
JavaScript
function printLabel(labelledObj) {
    console.log(labelledObj.label);
}

var myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

それでは実際にインタフェースを使ってみましょう。

TypeScript
interface LabelledValue {
    label: string;
}

function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
JavaScript
function printLabel(labelledObj) {
    console.log(labelledObj.label);
}

var myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

インタフェースはチェックにだけ使われて、コンパイルしたら消えるのね

コンパイラはプロパティがどのような順番になっていても気にしないことを覚えておいてください。

Optional Properties

必ずしもすべてのプロパティが必須とは限らないため、そのようなプロパティを表すにはプロパティ名の末尾に ? を付けてください。

TypeScript
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"});
JavaScript
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 のコードは特に変わらないか

任意のプロパティの利点は (プロパティ名の打ち間違い等で) インタフェースに定義されていないプロパティを誤って使用しないようチェックできる点です。

TypeScript
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 を付けてください。

TypeScript
interface Point {
    readonly x: number;
    readonly y: number;
}

この場合、オブジェクトリテラルで初期化することはできますが、変更することはできません。

TypeScript
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // エラー!

TypeScript では ReadonlyArray<T> クラスを用意しています。
これは Array<T> に似ていますが、オブジェクトを変更可能なメソッドが削除されています。

TypeScript
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // エラー!
ro.push(5); // エラー!
ro.length = 100; // エラー!
a = ro; // エラー!

最後の例は ReadonlyArray オブジェクトを普通の配列に代入できないことを表していますが、キャストを使用すれば可能です。

TypeScript
a = ro as number[];

readonly vs const

readonlyconst の使い分けは簡単です。
対象が変数であれば const を、プロパティであれば readonly を使ってください。

Excess Property Checks

インタフェースと任意のプロパティを組み合わせると、次のような落とし穴にハマることがあります。

TypeScript
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 ではオブジェクトリテラルに対して特別なチェックを行うようにしています。
具体的には、他の変数への代入時やメソッドの引数として渡された時に "対象の型" に含まれないプロパティが存在するとエラーとして扱います。

TypeScript
// エラー。'colour' は 'SquareConfig' に含まれない
let mySquare = createSquare({ colour: "red", width: 100 });

このチェックをバイパスするには単にキャストするだけです。

TypeScript
// 'SquareConfig' に 'opacity' は含まれないけど問題ない
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

もっと良いやり方は余分なプロパティを受け取るために文字列型のインデックスシグネチャを追加することです。

TypeScript
interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

'propName' と指定しているものの、別に 引数.propName で余分なプロパティにアクセスできるわけではない点に注意。
インデックスシグネチャについては後述

最後に紹介する方法は一度変数に代入することです。

TypeScript
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

単純なコードではそうは思わないかもしれませんが、コードがより複雑になってくると、これらのテクニックを使う必要が出てくるかもしれません。
ただし、多くの場合では、余分なプロパティの存在はバグであることを意識しておいてください。

Function Types

インタフェースは特定のプロパティを持つオブジェクトを表す以外に、メソッドの型を表すこともできます。
これには引数と戻り値だけを持つ関数宣言のように表現します。
この時、引数リストには名前と型が必須です。

TypeScript
interface SearchFunc {
    (source: string, subString: string): boolean;
}

後は他のインタフェースと同じように使用するだけです。

TypeScript
let mySearch: SearchFunc;

mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}

この時、引数の名前は必ずしも一致する必要はありません。

TypeScript
let mySearch: SearchFunc;

mySearch = function(src: string, sub: string): boolean {
    let result = src.search(sub);
    return result > -1;
}

型を指定していなくても、TypeScript は変数の型を元に型推論を行います。

TypeScript
let mySearch: SearchFunc;

mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

とはいえ、なるべく型は指定した方がいいと思う

Indexable Types

TypeScript では a[10]ageMap["daniel"] のように、インデックスでアクセスできる型も表現できます。
インデックス可能な型は、インデックスとして指定可能な型とその戻り値の型を指定した インデックスシグネチャ を持ちます。

TypeScript
interface StringArray {
    [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

インデックスシグネチャは 文字列型数値型 の 2 種類をサポートしています。
両方の型をサポートすることも可能ですが、その場合、数値型のインデックスシグネチャの戻り値の型は、文字列型のインデックスシグネチャの戻り値の型のサブクラスである必要があります。
なぜなら、JavaScript では数値型のインデックスは文字列に変換されてから使用されるためです。

TypeScript
class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// エラー。文字列型でアクセスすると Animal が返ってくることがある。
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

それ以外に、インデックスシグネチャはすべてのプロパティの型を統一させる効果もあります。

TypeScript
interface NumberDictionary {
    [index: string]: number;
    length: number;    // OK。length は数値型
    name: string;      // エラー。name は数値型またはそのサブクラスではない
}

最後に、インデックスシグネチャに readonly を指定することで、インデックスを指定した代入を禁止することもできます。

TypeScript
interface ReadonlyStringArray {
    readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // エラー!

Class Types

Implementing an interface

インタフェースのもっとも一般的な用途はクラス宣言に使用することです。

TypeScript
interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

同様にクラスで定義すべきメソッドを宣言することもできます。

TypeScript
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 種類のメンバを持つ点を意識しておいてください。

例えば、以下の例はエラーとなります。
なぜなら、インタフェースを実装する際にはインスタンスメンバしかチェックされないため、静的メンバであるコンストラクタがチェックの対象にならないからです。

TypeScript
interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

コンストラクタ(コンストラクタシグネチャ)を持つインタフェースは実装(継承)できないということかな?

代わりに静的メンバを直接使用する必要があります。
以下の例ではコンストラクタ用のインタフェース ('ClockConstructor') とメソッド用のインタフェース ('ClockInterface') をそれぞれ用意しています。

TypeScript
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

インタフェースはクラスのように拡張することもできます。

TypeScript
interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

また、複数のインタフェースを基に拡張することも可能です。

TypeScript
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

インタフェースでは、これまでに挙げた機能を組み合わせて使用することが可能です。

TypeScript
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 メンバを持つクラスを継承したインタフェースは、そのクラス自身、またはそのサブクラスでしか実装することはできないということです。

このことは特定のプロパティを持つサブクラスに対してのみ、動作するコードを作成したい場合に役立ちます。
この時、サブクラスで必要なことは基底クラスを継承することだけです。

TypeScript
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 メンバにアクセスすることができます。
そのため、SelectableControlselect メソッドを持つ Control クラスのように振る舞います。
ButtonTextBox クラスは ('Control' クラスを継承し、かつ、select メソッドを持っているため) SelectableControl のサブクラスですが、ImageLocation クラスは違います。

TextBoxselect メソッドを持たないから、SelectableControl ではないような気が...

21
15
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
21
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?