Edited at

Decoratorで拡張しているクラスにFlowで型をつける

これは型がないNode.jsのWebアプリケーションに対してFlowで型を導入したときにDecoratorで追加拡張している関数やプロパティがないって怒られたのでその時行った対策のメモです。

もっと良い方法を知っておられる方がいましたら、ご教授ください。


はじめに


Flowとは

型なき世界のためのflowtype入門 - Qiitaとか有名な記事を見てください。


Decoratorとは

まだEcmaScript stage2の機能です。proposal


version

Node.js: v8.11

flow-bin: ^0.77.0


問題となったケース

Decoratorを使ってプロパティや関数を追加しているクラスが問題となりました。

例えば以下のようなDecorator関数があります。


externalEngine.js

export function ExternalEngine(name, power) {

return function decorator(Class) {
Object.assign(Class.prototype, {
engineName: name,
power: power,
external: true,
changePower: (power) => {
this.power = power;
}
useExternal: () => {
return this.speed ** this.power;
}
});
}
}

そのDecorator関数を使ったクラスがあります。


MyCar.js

@ExternalEngine("UltraEngine2", 4)

class MyCar {
speed;
dash: () => {
this.speed = this.external ? this.useExternal() : this.speed * 2;
}
}

そしてこのクラスにFlowで型定義を書きます。


MyCar.js

// @flow

@ExternalEngine("UltraEngine2", 4)
class MyCar {
speed: number;
dash: () => {
this.speed = this.external ? this.useExternal() : this.speed * 2;
}
}

そしてflowを実行すると、エラーになります。


エラー内容と対応策


そもそもDecoratorは実験的な機能だと怒られる

以下のようなエラーが出ます。

Experimental decorator usage. Decorators are an early stage proposal that may change. Additionally, Flow does not

account for the type implications of decorators at this time.


対応策

.flowconfigファイルのoptionsに以下を追加してください。

[options]

esproposal.decorators=ignore


そんなプロパティは存在しないと怒られる

以下のようなエラーが出ます。

Cannot get this.external because property external is missing in MyCar [1].

Cannot call this.useExternal because property useExternal is missing in MyCar [1].

と出力されます。

Decoratorで拡張したプロパティや関数が認識されていないためです。


対応策

結構苦肉の策でしたが、親クラスとなるクラスを型定義ファイルに書いてそれをComment Typesで継承しました。

以下のような型定義ファイルを作成します。


externalEngine.js.flow

declare class ExternalEngine {

engineName: string;
power: number;
external: boolean;
changePower: (number) => void;
useExternal: () => number;
}

そしてそれをMyCar側はComment Typesで継承します。


MyCar.js

// @flow

@ExternalEngine("UltraEngine2", 4)
class MyCar /*:: extends ExternalEngine */ {
speed: number;
dash: () => {
this.speed = this.external ? this.useExternal() : this.speed * 2;
}
}

これでFlowが実行するときはMyCarはExternalEngineのサブクラスとして認識されます。


最後に

コメントとはいえ継承を用いるのはDecoratorと使いところが違いますし、良くないなーと言うのが本音ですが、型定義のために今動いているコードを変更するのもしんどいなと感じたのでこのような対応策を選びました。

本当はFlowがDecoratorでの拡張を認識して良しなにしてくれるかDecorator用の型定義ファイルを置いとけばそこから判定するようになってくれれば良いのかなと思います。

他に良い方法があればどんどんコメント欄に書いてほしいです。よろしくお願いいたします。

最後までお読みいただきありがとうございました。質問や不備はコメント欄またはTwitter(@shisama_)からお願いいたします。

(今回出てきたサンプルコードはこの記事のために適当に書いたものです。実際運用しているコードではありません。また、サンプルコードの品質については記事の本質ではないのでおやめください)