バイクとプログラミング

月: 2019年12月

TypeScriptで型安全なEventEmitterを実現する

Nodejsでイベント駆動プログラミングをするときに便利なのがevent.EventEmitterですが、これをTypeScriptで利用しようとするとうまく型安全にできません。

const event = new EventEmitter();
event.on('foo', arg => console.log(arg)); 
// argは必然的にanyとなる...
event.emit('foo', 123);
event.emit('foo', '123');
event.emit('foo', {}); // なんでもあり

この問題に対する解決策は以前からいくつか存在していましたが、どの方法もデメリットがある状態でした。

しかし、TypeScript 3.0から導入されたConditional Typesを利用することによりほぼ完全なイベントの型付けを実現できるようになりました。

今回は、それをライブラリとして利用しやすくしたstrict-event-emitter-typesを利用したので、忘備録として紹介します。

簡単な使用方法

大体README.mdに書かれている内容と同じです。

事前の準備として、イベントの型情報をまとめたインターフェースと、EventEmitterを取得します。


import StrictEventEmitter from 'strict-event-emitter-types';

// イベントの定義
interface Events {
  request: (request: Request, response: Response) => void;
  data: String; // (data: String) => void; のショートハンド
  done: void; // 引数なし
}

import { EventEmitter } from 'events';

// イベントエミッタとイベント定義を元にStrictEventEmitterを作成
const ee: StrictEventEmitter = new EventEmitter();

この方法で生成されたEventEmitter(サンプル内ee)は、全て型安全で利用することができます。

利用方法は普段のEventEmitterと同様です。


ee.on('request', (req, res) => ... );
// reqはRequest, resはResponseとして認識される。

ee.on('something', () => ... );
// イベントが存在しないのでコンパイルエラー

ee.on('done', hoge => ... );
// doneには引数がないのでコンパイルエラー

ee.emit('data', 'こんにちは!');
// OK

ee.emit('data', 12345);
// 型が合わないのでコンパイルエラー

簡単ですね。

ただし、TypeScriptの動作により、イベント定義インターフェースに不具合があった場合に不明瞭なエラーメッセージが出現します。

“not assignable to parameter of type ‘unique symbol'” error · Issue #9 · bterlson/strict-event-emitter-types

これはTypeScript側で修正予定だそうです。

Assignability of callback parameter defined with conditional tuple type · Issue #26013 · microsoft/TypeScript

strict-event-emitter-typesをラップする方法について

プルリクエストを送ってある件ですが、元ソース中のListenerTypeがExportされていないため、このライブラリのラッパを作成することができません。

Export ListenerType by kznrluk · Pull Request #13 · bterlson/strict-event-emitter-types

ローカルで編集してしまうか、私のPR元ブランチをクローンして使ってください。

kznrluk/strict-event-emitter-types: A type-only library for strongly typing any event emitter

JavaScript FetchAPIでのPOSTはContent-Typeを指定しない方が良い

TLDR: JSON以外はContent-Type不要。

以前作業したコードで、呼び出されたらAjaxを用いて通信し、レスポンスに対応した適当な処理を挟んでから呼び出し元にデータを渡す、いわゆるラッパクラスのようなものが存在する。

今まで、これらの処理はjQuery.ajax()を利用してきたが、今時jQueryというのもよくないのでFetchAPIへ移行する運びとなった。

新しいコードへの移行はかなり簡単に終了することができたが、FetchAPIのContent-Type指定で多少つまずきがあったのでメモ。

FetchAPIでは、POSTを行う際、bodyに指定されたデータの型が主要なものであればContent-Typeもそれに合わせて自動的に指定される。例えばStringであればtext/plain;charset=UTF-8, URLSearchParamsであればapplication/x-www-form-urlencoded;charset=UTF-8といった具合である。これは仕様に記載されている動作である。

Fetch Standard – https://fetch.spec.whatwg.org/#body-mixin

また、Content-Typeを手動で指定した場合、そちらが優先される。例えばJSON.Stringify()の返り値はStringであるが、Content-Typeを指定すれば勝手にtext/plainにされることはなくなる。

この仕様のため、JSONを除けば大概の送信手段はContent-Typeを指定する必要がない。指定してしまうと、データの型と相違があった場合にサーバがリクエストボディをうまく解釈できない場合がある。

特にFormDataの場合、手動でヘッダを指定するとboundaryを無視することになる。そのためデータがサーバに届いたとしても、それを解釈することができない。これのため一時間ほどハマった。

余談として、当時どうしてもうまく送信できなかったのでFetchのPolyfill実装を読んでみたが、BlobSearchParamsに関する記述はあるのにFormDataに関する記述が無く困った。XHRでFormDataを判別してContent-Typeに指定しているのか?そのうち調べてみたい。

fetch/fetch.js at master · github/fetch – https://github.com/github/fetch/blob/master/fetch.js#L233

以上です。

Powered by WordPress & Theme by Anders Norén