皆さんイベントを扱ってますか!!
JavaScriptではイベントは避けては通れない道ではありますが、イベントを送る、受け取るのには文字列でイベント名を指定してあげる必要があります。
このイベント名を指定するというのが曲者で、文字列での指定になるためどうしてもtypoが発生し、イベントの送受信が上手くいかない!!という事態になりやすかったりします。
さすがに目視でtypoチェックとかつらすぎるので、ここはFlowtypeを使って型の力でこの問題を解決してしまいます。
Flowtype
Flowtypeはstring literal typeと関数のoverloadに対応しています。
string literal type
var foo: "foo" = "bar"// -> Error!!
overload
declare class Foo {
bar(a: string): void;
bar(a: number, b: number): void;
}
というわけで、この2つの機能を使ってElectronのappのイベントを定義してみます。
declare class ElectronApp {
on(evt: 'will-finish-launching' | 'ready' | 'window-all-closed' | 'gpu-process-crashed' | 'platform-theme-changed', listener: () => void): EventEmitter;
on(evt: 'before-quit' | 'will-quit', listener: (event: Event) => void): EventEmitter;
on(evt: 'quit', listener: (event: Event, exitCode: number) => void): EventEmitter;
on(evt: 'open-file', listener: (event: Event, path: string) => void): EventEmitter;
on(evt: 'open-url', listener: (event: Event, url: string) => void): EventEmitter;
on(evt: 'activate', listener: (event: Event) => void, hasVisibleWindows: boolean): EventEmitter;
on(evt: 'browser-window-blur' | 'browser-window-focus' | 'browser-window-created', listener: (event: Event, window: BrowserWindow) => void): EventEmitter;
on(evt: 'certificate-error' | 'select-client-certificate', listener: (event: Event, webContents: WebContents, url: string, error: string, certificate: {data: Buffer, issuerName: string}, callback: Function) => void): EventEmitter;
on(evt: 'login', listener: (event: Event, webContents: WebContents, request: {method: string, url: string, referrer: string}, authInfo: {isProxy: boolean, scheme: string, host: string, port: number, realm: string}, callback: Function) => void): EventEmitter;
}
declare module electron {
declare var app: ElectronApp;
}
この定義を使用すると
import {app} from 'electron';
app.on('ready', () => { }); // no error
app.on('ready1', () => { }); // error
というように未定義のイベント名に対してエラーを発生させることができるようになります。
さらに、listener
の引数も定義しているので
import {app} from 'electron';
app.on('quit', (event: Event, exitCode: number) => {}); // no error
app.on('quit', (event: Event, exitCode: string) => {}); // error
というようにlistener
に渡す関数の型チェックも行えます。
これでうっかり別の関数をlistener
に渡してしまうなんていうミスもなくせるので素敵ですね!
Atom+Nuclide
ここまでしっかりと型定義をし始めると、開発効率を上げるためにコード補完しつつ、C++やJavaのように型のミスマッチをすぐに警告して欲しくなってきます。
いくつかFlowtype対応をうたってるIDEやエディタを試してみたところ
この組み合わせが一番自分にマッチしていました。(というか他のがFlowtypeの設定を書く.flowconfig
に対応してないのばかりでコレ以外に選択肢がありませんでした)
Atom+NuclideをインストールしてFlowtypeのコードを書くとこのように軽快に補完しつつ、型チェックを行うことが出来るようになります。
これで手軽に、かつ安全にイベントを扱えるようになって最高ですね!!!
つらい話
最高ですねで済めば幸せだったのですが、まぁそんな上手い話はない訳で。
正直なところ型定義がつらいです。
d.tsではここまで型定義されていないので持ってきてFlowtypeの型定義に直すということはできませんし、ドキュメントにイベントで渡される引数がなかったり、実は違う引数があったりで、コードを読む必要が出てきたりで型定義がかなり面倒臭いです。
また、Flowtypeでは型定義と実際のコードが分かれて管理する必要があるため、型定義の保守を怠ると簡単に型と実際に動作するコードが乖離しだしてしまいます。
型安全が欲しくて型定義を書いてるのに、実際に動かすと違う型が渡されて型定義の意味がないというつらみがあります。
(Scala.jsみたいに実行時に型の検証すべきなのかなぁ)
何か上手い方法が欲しいです。
おわりに
少し前に
という記事でもイベントを扱いましたが、ライブラリが発火するイベントを扱うなら今回の方法がスマートです。(普通にイベントを書けば型が効きますし)
逆に自作のイベントを扱うならコードとイベントを乖離させずにイベントを定義できるPhantom Typeの方が使い易いパターンもありそうです。
このあたりで、こう書くとより安全に書けるよ!などあればぜひぜひ教えてください。