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

flowtypeでのAny TypeとUnknown TypeとMixed TypeとExistential TypeとGenericsの話

More than 3 years have passed since last update.

anyなんてこの世から消えてしまえば良いんだ。

と、思ったのですが、flowtypeでanyと似たような用途で使うけど、明確にどう使い分けているのか理解できていない型があると気づいたので一度情報をまとめてみようと思います。

Any Type

var anyType: any;

Any Typeは$Taintedを除いた全ての型を受け取り、全ての型に変換できる特殊な型になります。
平たく言ってしまえばAny Typeを利用するとflowtypeによる静的型付けな型チェックが行われなくなります。

var anyType: any = 1;
anyType.toFixed();
anyType.replace('', ''); // Not Erorr

例として上記のように存在しないメソッドの呼び出しでエラーになるべきコードで型チェックで検出することができず、実行時のエラーとなってしまいます。

型チェックをするためにflowtypeを導入しているはずなのに、Any Typeで型チェックを行わなくするという不可思議な状況に陥るため、基本的に明確な理由がない場合はAny Typeは使われるべきではないと思っています。

それでは、どういった時にAny Typeは使われるべきでしょうか?
自分は下記の2点をAny Typeを利用するときの判断としています

  1. 型定義の存在しないライブラリを利用していて、自分で型定義を書くコストが高いとき
  2. 実装を強制的に型定義に合わせたいとき

1.は例えば開発工数が確保できていないときや、変更頻度が高く型定義を自分で書いて維持するコストが高くて割に合わないときです。
まぁ、そういうこともあるよね。という感じではありますが、この場合もできる限り後述のMixed TypeやType Refinementsを利用して回避します。

2.はどうしても実装と型定義を合わせることができない・・・というときに最終手段として使うことがあります。

// @flow
const crypto = require('crypto');

let cache: {[key: string]: Function} = {};

module.exports = function memorize<S: Function, T: S>(f: S): T {
  let fString = f.toString();
  return (function() {
    let aString = JSON.stringify(Array.from(arguments));
    let key = hash(`${fString}:${aString}`);

    if (cache[key] !== undefined) {
      return cache[key];
    } else {
      return cache[key] = f.apply(this, arguments);
    }
  }: any);
};

function hash(plaintext: string): string {
  let hashsum = crypto.createHash('sha256');
  hashsum.update(plaintext);
  return hashsum.digest('hex');
}

上記はちょっと前に書いたコードですが、どうしても型定義と合わせることができず、諦めてAny Typeを利用して実装と型定義に合わせています。(もし上手な書き方があれば教えてください)
このときに本当にAny Typeを利用するかの判断の基準として関数内でAny Typeで収まること、ユニットテストを強化して型通りの挙動をすることの担保が取れていること、の2点を満たしていたら利用するようにしています。

Any Typeは便利なものではあるのですが、他に代用する手段はないのか、よく考えてから利用してください。

Unknown Type

Any Typeと同じ挙動をするものとしてUnknown Typeというものがあります。(Unknown Typeは正式名称ではなくflow type-at-pos(unknown)と表示されるため便宜的にそう呼んでいます)
これはflowtypeで推論ができなかったときに発生する型になります。

class MemoryStore<O: Object> {
  _data: $Shape<O>;

  constructor(data: $Shape<O>) {
    this._data = data;
  }

  set<K>(key: K, value: $ElementType<O, K>): void {
    this._data[key] = value; // value is Unknown
  }
}

例えば、上記のコードsetの引数で渡されるvalueがUnknown Typeにあたります。
これはflow checkでは検出できず、flow type-at-posで該当箇所をチェックするか、flow coverageでカバレッジが取得できないことを確認するしかありません。

もし、手軽にチェックをしたいのならAtomのNuclideを利用するとflow coverageの結果をコード上で表示できるためおすすめです。
image.png
意図的に書いたAny Typeでも警告が表示されるため若干面倒臭いですが、意図せずUnknown Typeになることを防ぐためにも開発時は常時有効にしておくと良いです。

正直、Unknown Typeが発生した場合はエラーにしてくれるオプションが欲しいですね。

Mixed Type

Any Typeを置き換えるさいに真っ先に思いつくのはこのMixed Typeになります。
Mixed TypeはAny Typeと同様に$Taintedを除いた全ての型を受け取ることができますが、型を利用するのにはType Refinementsを強制します。

var mixedType: mixed = 1;
mixedType.toFixed(); // Error
mixedType.replace('', ''); // Erorr

Any Typeと同様の例の場合では代入は可能ですが、メソッドの呼び出しに失敗します。
もし、メソッドの呼び出しを行いたい場合は

if (typeof mixedType === 'number') {
  mixedType.toFixed();
}

のようにType Refinementsを行います。
このようにType Refinementsを強制することでAny Typeのように実行時エラーが発生してしまうことを防げることがMixed Typeの最大のメリットになります。

大部分のAny TypeはこのMixed Typeに変換することでより安全なコードにできます。
もし、Any Typeを使いたいと思ったときは、まずこのMixed Typeで代用できないか考えてみてください。

Existential Type

flowtypeにはExistential Typeという推論を強制する型があります。

var existentialType: * = 1;
existentialType.toFixed();
existentialType.replace('', ''); // Error

上記の場合はexistentialType1を代入しているためnumber型として認識することができます。(ただ、上記の例ではExistential Typeを使わなくても推論してるのでありがたみがないですけど)

自分がExistential Typeを使うときは構文的に型が必要だが書けない or 面倒臭いときに利用することが多いです。

declare class Foo<A, B: $Keys<A>> {
  constructor(data: A): void;
}

declare var foo: Foo<{}, *>;

上記のようにGenericsで複数の型パラメータを取る必要があるけど、1つ渡せば決定できるときや

class Foo {
  data: *;

  constructor() {
    this.data = { ... };
  }
}

のようにプロパティの初期化する場合など、型定義が必要なところで省略したいときに利用することが多いです。

Any Typeと違い、Existential Typeは型情報を(推論が出来るかぎりは)失われないため使い勝手は良いです。

ただし、プロパティの初期化の例などでは初期化時のtypoでキー名を間違えた、値を間違えたなどで想定の型と違う型になりえます。
そのため、適切な型定義を事前に用意した方が望ましいのはたしかです。

大抵の場合は型を利用するために別の手段が存在するので、そちらを採用することが多くなると思います。
もし、こういうときはExistential Typeを使わないと表現できないというものがあれば情報提供をお願いします。

Generics

クラスや関数でどんな値でも受け取りたいときはGenericsを使うことが多いです。

function arrayWrapper(value: any): Array<any> {
  return [value]
}

上記のようにAny Typeで表現することも可能ですが、そうすると型情報が失ってしまうため

function arrayWrapper<T>(value: T): Array<T> {
  return [value]
}

のようにGenericsを使うことが多いです。
今回の例のような副作用のない単純な関数では、Genericsを使えば実装は1種類しか書けないため、型で実装を縛ることができるというメリットもあります。

ちなみにflowtypeのドキュメントにあるExistential Typeの例も同様にGenericsで書く方がスマートです。

function makeParamStore<T, P: ParamStore<T>>(storeClass: Class<P>, data: T): P {
  return new storeClass(data);
}
(makeParamStore(ParamStore, 1): ParamStore<number>);
(makeParamStore(ParamStore, 1): ParamStore<boolean>); // Error

たしかにドキュメントの通りExistential Typeでも期待した戻り値の型になるのですが、例えばreturn new storeClass(data);return 'foo';とすると関数の型チェックとしてはエラーにならずに通過してしまいます。
それを防ぐためにGenericsを利用して実装ミスを検出できるようにするというのは重要になります。

この型で実装を縛るという考え方は結構好きで、自分は動的型付け言語にばかり触っていたので、この考えを聞いたときはそんな考えは思いもしなかった!!と衝撃的でした。
どうすれば型で実装を縛れるか意識を始めると、より静的型付けの言語が楽しくなってくるのでおすすめです。

おまけでvoidについて

そういえば前にメソッドの戻り値がvoidなのに、any的な挙動しててvoid怖いと思ったことを思い出しました。

fixes type check in todomvc-flow by k-kinzal · Pull Request #102 · almin/almin

たしか、@flowをつけ忘れたときは型定義ではなく実装からの推論が優先されてvoidであることのチェックがされないというものですね。
voidは悪くはないのですが、@flowを忘れるとそういった意図しない挙動をするので注意が必要です。

@flow の有無を eslint でチェックする - Qiita

eslintを使って@flowの有無をチェックできるので、flowtypeで開発中は有効にしておくことをオススメします。

まとめ

改めてAny Typeについて考えてみましたが、Any Typeを使いたいときはだいたい妥協してるときですね。
普通に考えればflowtypeでわざわざ動的型付けのJavaScriptから静的型付けの世界に入りに来ているのに、それをぶち壊すような真似をするのはおかしい訳で。

早くAny Typeを使ったら夜道で刺されるぐらいの世界になって欲しいものですね。

kinzal
PHPやったり、JSやったり、AWSやったり、CI環境構築したり、絵描したり、幽体離脱したり。そんな人。
http://about.me/kinzal
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