yaakaito.org

TypeScriptのInterfaceとStructural Subtyping

TypeScript

こんにちは!うきょーです! TypeScriptにはInterfaceとStructural Subtypingがあるのでそれについて書こうと思います。まだ0.8.x系です。

Interface

JavaやC#でいうinterfaceと似ていますが、ある形をしたオブジェクトに対する別名というのが正しいと思います。

1
2
3
4
5
6
7
8
9
10
11
interface Rider {
    name: string;
    henshin: () => void;
}

var wizard: Rider = {
    name: '仮面ライダーウィザード',
    henshin: () => {
        console.log('シャバドュビタッチヘンシーン!');
    }
}

こうすると wizardRider であることになります。 JavaScriptには存在しないシンタックスなので、コンパイルするとinterfaceは当然ですが消えます。

1
2
3
4
5
6
var wizard = {
    name: '仮面ライダーウィザード',
    henshin: function () {
        console.log('シャバドュビタッチヘンシーン!');
    }
};

同じように beast も作ってみます。

1
2
3
4
5
6
var beast: Rider = {
    name: '仮面ライダービースト',
    henshin: () => {
        console.log('L・I・O・N!ライオーン!');
    }
}

やりましたね。

このinterfaceはそのまま型のように扱うことが出来るので、

1
2
3
4
5
6
var please = function(rider: Rider) {
    rider.henshin();
}

please(wizard);
please(beast);

こういう風に使う事も出来ます。便利ですね。

もちろんclassにimplementsすることも出来ます。

1
2
3
4
5
6
7
class WizardInfinity implements Rider {
    public name: string = '仮面ライダーウィザード インフィニティ';

    public henshin(): void {
        console.log('ヒースイフードーボーザバビュードゴーン!');
    }
}

実装が欠けている場合は、こういう感じのエラーになります。

1
2
Class 'WizardInfinity' declares interface 'Rider' but does not implement it: Type 'WizardInfinity' is missing property 'name' from type 'Rider'
Class 'WizardInfinity' declares interface 'Rider' but does not implement it: Type 'WizardInfinity' is missing property 'henshin' from type 'Rider'

Functionにも適用することができて、

1
2
3
4
5
6
7
8
9
10
11
12
13
class Ring {
    public name: string = 'ドライバーオンウィザードリング';
}

interface DriverOn {
    (ring: Ring): void;
}

var driverOn: DriverOn = function(ring: Ring) {
    if (ring.name === 'ドライバーオンウィザードリング') {
        console.log('ドライバーオーンッ!プリィィィズ!');
    }
}

こういう感じに書けます。これだけだと別に利点が分からなくて、

1
2
3
4
5
var driverOn = (ring: Ring): void => {
    if (ring.name === 'ドライバーオンウィザードリング') {
        console.log('ドライバーオーンッ!プリィィィズ!');
    }
}

これで別によくね、となるんですが、いうならオーバーロードみたいなことが出来るのでちょっと便利です。こういう感じで書く事が出来るので、

1
2
3
4
interface DriverOn {
    (ring: Ring): void;
    (love: Love): void;
}

全体でこんな感じになってると、Phantomを受け付けません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Ring {
    public name: string = 'ドライバーオンウィザードリング';
}

class Love {
    public target: string = 'こよみ';
}

class Phantom {
    public original: string = 'ミサ';
}

interface DriverOn {
    (ring: Ring): void;
    (love: Love): void;
}

var driverOn: DriverOn = function(power: any) {
    if (power instanceof Ring && power.name === 'ドライバーオンウィザードリング') {
        console.log('ドライバーオーンッ!プリィィィズ!');
    }
    else if (power instanceof Love && power.target === 'こよみ') {
        console.log('インッフィニティィ!インッフィニティィ!インッフィニティィ!インッフィニティィ!');
    }
}

driverOn(new Ring());
driverOn(new Love());
driverOn(new Phantom()); // Supplied parameters do not match any signature of call target

あとは interface A extends B と出来たり、同じinterfaceを2回定義する事で拡張できたりします。

Structural Subtyping

ようするにダックタイピングみたいなやつです。 さっきのplease関数の引数を、

1
2
3
function please(rider: { name: string; henshin: () => void; }): void {

}

みたいに出来ます、言ってしまえばそれだけです。

なんの役に立つかよくわかんないと思うので、each関数みたいなのを作るのを想定してみましょう。 ウィザードで説明するのに限界を感じ(飽き)ました。

eachって基本的にはArrayを受け取ると思うんですが、JavaScriptにはArrayっぽいものというのが存在しますよね。 オブジェクトとしては、数値でインデックスアクセスできて、lengthを持っている、というものです。

で、これをさっきの例に当てはめると、

1
2
3
function each(list: { [index: number]: any; length: number; }, func: (obj: any) => void): void {
    return [];
}

まあこんな感じになるわけです。 これで例えばArrayっぽいargumentsとかが通るようになります。

1
() => each(arguments, (obj: any) => {})

ただこのままだと、map([1,2,3])が通らないので、any[]も受け付けるようにオーバーロードします。

1
2
3
4
5
6
7
function each(list: any[], func: (obj: any) => void): void;
function each(list: { [index: number]: any; length: any; }, func: (obj: any) => void): void;
function each(list: any, func: (obj: any) => void): void {
    for (var i = 0, l = list.length; i < l; i++) {
        func(list[i]);
    }
}

こんな感じ。長い。こうすると

1
2
each([1,2,3], (obj: any) => {})
() => each(arguments, (obj: any) => {})

は通るけど、

1
each(1, (obj: any) => {})

とかは通らなくなります、よかったですね!

あとはfuncをInterfaceにまとめて、

1
2
3
4
5
6
7
8
interface EachFunc {
    (obj: any): void;
}

function each(list: any[], func: EachFunc): void;
function each(list: { [index: number]: any; length: any; }, func: EachFunc): void;
function each(list: any, func: EachFunc): void {
}

こうなって、さらにさっきのをInterfaceを経由するようにすると、

1
2
3
4
5
6
7
8
9
10
11
12
13
interface EachFunc {
    (obj: any): void;
}

interface LikeArray {
    [index: number]: any;
    length: any;
}

function each(list: any[], func: EachFunc): void;
function each(list: LikeArray, func: EachFunc): void;
function each(list: any, func: EachFunc): void {
}

とか書いたりできます。

まとめ

ヒースイフードーボーザバビュードゴーン!