Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

これは型がない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_)からお願いいたします。

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

shisama
Node.js Core Collaborator. 関西Node学園Organizer.
https://shisama.dev
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away