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.は例えば開発工数が確保できていないときや、変更頻度が高く型定義を自分で書いて維持するコストが高くて割に合わないときです。
まぁ、そういうこともあるよね。という感じではありますが、この場合もできる限り後述の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
の結果をコード上で表示できるためおすすめです。
意図的に書いた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
上記の場合はexistentialType
に1
を代入しているため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を使ったら夜道で刺されるぐらいの世界になって欲しいものですね。