Posted at
CureAppDay 11

flowのinterfaceでハマる

More than 1 year has passed since last update.

flowのinterfaceは便利だが、2016/12 時点では、

気をつけないといけない不具合がある。

本記事で利用したflowのバージョンはv0.34.0。


flowのinterface

JSの静的型チェッカであるflowは、interfaceも定義できる。

https://flowtype.org/docs/syntax.html#interface-declarations


Interface declarations

Declaring an interface creates a type that multiple classes can satisfy without being part of the same inheritance hierarchy.

Like type aliases, interfaces are entirely erased at compile time and have no runtime presence.


「インターフェイス宣言は、継承の階層関係なしに、複数のクラスが満たす型を生成します」と。


使う

何か、転がして、数値か文字列を得るという仕様を、

Rollableというインターフェイスとして定義。

// @flow

interface Rollable {
roll(): number | string;
}

サイコロ Diceクラスを下記のように定義。数値が返る。


class Dice {
roll(): number {
return Math.floor(Math.random() * 6) + 1
}
}

ごきげんようの、文字のサイコロを、下記のように定義。

class Gokigenyo {

roll(): string {
const values = ['情けない話', '恋の話', '輝いてた話',
'怖い話', '笑える話', 'あたりめ']
return values[Math.floor(Math.random() * 6)]
}
}

そうすると、2つのクラスののインスタンスは、Rollable型とも解釈される。

const rollable1: Rollable = new Dice()

const rollable2: Rollable = new Gokigenyo()

戻り値の型が違くても、うまくまとめてくれた。抽象化できてうれしい


プロパティ型は厳密一致を強いる

↑のコードを以下のように書き換えると、うまくいかない。

まず、「当たり」のように特別な値specialValueがあるという仕様を、

SpecialRollableというインターフェイスとする。

// @flow

interface SpecialRollable {
specialValue: number | string;
roll(): number | string;
}

サイコロは、6が特別な値とする。


class Dice {
specialValue: number;

constructor() {
this.specialValue = 6
}

roll(): number {
return Math.floor(Math.random() * 6) + 1
}
}

ごきげんよう、は「あたりめ」が特別な値とする。


class Gokigenyo {

specialValue: string;

constructor() {
this.specialValue = 'あたりめ'
}

roll(): string {
const values = ['情けない話', '恋の話', '輝いてた話',
'怖い話', '笑える話', 'あたりめ']
return values[Math.floor(Math.random() * 6)]
}
}

で、下記のようにつかってみると...

const rollable: SpecialRollable = new Dice()

  4:   specialValue: number | string;

^^^^^^ string. This type is incompatible with
9: specialValue: number;
^^^^^^ number

型で怒られた。

このエラーは、specialValuenumber | string型で来て欲しいのに、number型で来てるよというもの。

戻り値の型は、包含関係にあれば許容していたが、プロパティの型は、厳密一致を強いるようになっている。


嬉しくない解決策1

インターフェイスに、型パラメータを導入して、利用するときに入れる。

// @flow

interface SpecialRollable<T: number | string> {
specialValue: T;
roll(): T;
}

const rollable1: SpecialRollable<number> = new Dice()

const rollable2: SpecialRollable<string> = new Gokigenyo()

利用側がクラスを意識しないといけないので、あまり嬉しくない。


嬉しくない解決策2

any型にすればなんでも許されるというもの。

// @flow

interface SpecialRollable {
specialValue: any;
roll(): number | string;
}

specialValueが定義されていないクラスは許容しないので、プロパティ名が存在するという仕様は強いることができる。nullも許容するけど。


オブジェクト定義も、プロパティは厳密一致を強いる

嬉しい解決策を提示しないどころか、もう一個ダメな例を紹介。

const rollable: SpecialRollable = {

specialValue: 1,
roll() { return 1 }
}

クラスを介さず、直接オブジェクトを書いても、やはりプロパティの型が厳密一致でないため怒られる。


これには解決策があった

インターフェイスを使わず、type aliasを使えば怒られなくなった。

type SpecialRollableType = {

specialValue: number | string;
roll(): number | string;
}

const rollable: SpecialRollableType = {

specialValue: 1,
roll() { return 1 }
}

関連issue #1569を参照のこと。


関連issue

これらをみると、この挙動は意図されたものではなく、修正予定である。