TypeScript 2.7 で FileReader
を使うコードを書くときに型情報が不足しているのが気になったので Microsoft/TSJS-lib-generator に PR #398 を出して TypeScript 2.9 に取り込んでもらった話です。
事の発端は TypeScript 2.7 で書いていたこのようなコードです。
const reader = new FileReader()
reader.onload = (event) => {
// Property 'result' does not exist on type 'EventTarget'.
const url = event.target.result
}
reader.readAsFileURL(file)
MDN の FileReader.onload にあるようなシンプルなファイル読み込みなのですが、これを書くと「event.target.result
が無いよ」と言われてしまいました。
同じような問題を訴えている TypeScript の issue もあったので、よっしゃ黙らせたるってことで、TypeScript 自体の FileReader に手を入れてみることにしました。
どこに手を入れればいいのか探す
FileReader.onload
は node_modules/typescript/lib/lib.dom.d.ts
でこのように定義されています。
interface FileReader extends EventTarget {
// ...
onload: ((this: FileReader, ev: ProgressEvent) => any) | null;
// ...
}
この ev
に target: EventTarget | null
が生えています。その target
に result
を生やせれば、event.target.result
と書いても何も言われなくなります。イメージとしてはこんな形を望んでいます。
interface FileReader extends EventTarget {
// ...
onload: ((this: FileReader, ev: FileReaderProgressEvent) => any) | null;
// ...
}
interface FileReaderProgressEvent extends ProgressEvent {
result: FileReader | null;
}
こうするには TypeScript に入っている lib.dom.d.ts
に手を入れる必要があります。そうするにはどうすればいいでしょうか。
TypeScript の README にある Contribute という項目からは CONTRIBUTING.md へのリンクが貼られています。ここに書いてあることに従っておけばよさそうです。
この資料の中を lib で検索すると、すぐに Contributing lib.d.ts fixes という項目に引っかかるはずです。そこには
- ライブラリのソースファイルは
src/lib
にあるよ -
src/lib/dom.generated.d.ts
は自動生成されるので、変更したい場合は https://github.com/Microsoft/TSJS-lib-generator に PR を出してね
と書いてあります。lib/README.md
によると、src/lib/dom.generated.d.ts
は lib/lib.dom.d.ts
を生成するために使うファイルです。
というわけで lib.dom.d.ts
を書き換えるには src/lib/dom.generated.d.ts
を書き換える必要があり、それを書き換えるためには TSJS-lib-generator に手を入れる必要があるというわけです。ふー、長かった。
TSJS-lib-generator とは
TSJS-lib-generator とは、リポジトリの説明にある通り、DOM に関連した TypeScript と JavaScript ライブラリファイルを生成するツールです。この Contribution Guidelines に、やるべきことが丁寧に書いてあります。短いので、和訳したものを置いておきます。
Contribution Guidelines
TypeScript リポジトリの dom.generated.d.ts
, webworker.generated.d.ts
, dom.iterable.generated.d.ts
ファイルをベースラインとして使います。プルリクエストごとにスクリプトを走らせて、生成されたファイルとベースラインのファイルを比較します。テストを通すためにプルリクエストではベースラインを更新してください。
通常の変更は json ファイルを変更するだけで済みます。型の生成には主に addedTypes.json
, overridingTypes.json
, removedTyes.json
の3つのファイルを使います。comments.json
ファイルで型にコメントを足すことができます。最後に knownWorkerTypes.json
ではどの型が WebWorker 環境で使えるかを決めます。
それぞれのファイルのフォーマットは内容から察してください。
プルリクエストを送る一般的なステップは:
- TypeScript リポジトリ の issue を作るか参照する
- 足りない要素を
inputfiles/addedTypes.json
に追加したり、inputfiles/overridingTypes.json
で要素を上書きしたり、inputfiles/removedTypes.json
で要素を削除する - ローカルでビルドスクリプトを実行して、新しい
dom.generated.d.ts
とwebworker.generated.d.ts
を作る -
generated
フォルダに出力されたファイルを使ってbaselines
フォルダのファイルを更新する (npm run baseline-accept
)
(訳注:原文ではステップ 0 から始まるのですが Qiita だと 1 に書き換えられてしまうようです)
以上、和訳でした。というわけで、このステップをたどって FileReader.onload
の引数の型を書き換えていきます。
1. TypeScript の issue を作るか参照する
書いてある通りです。この時は FileReader.onLoad/onLoadEnd event argument has no target.result property という issue があったので、これを参照する形にしました。
2. json ファイルを編集する
FileReader.onload
の型を変えるために inputfiles/overridingTypes.json
を編集します。inputfiles/overridingTypes.json
の中を探すと FileReader
についてこのように書かれています。
"FileReader": {
"name": "FileReader",
"properties": {
"property": {
"result": {
"name": "result",
"override-type": "any"
}
}
}
},
今回は onload
を (this: FileReader, ev: FileReaderProgressEvent) => any
にしたいので、このように書きます。
"FileReader": {
"name": "FileReader",
"properties": {
"property": {
"onload": {
"name": "onload",
"override-type": "(this: FileReader, ev: FileReaderProgressEvent) => any"
},
"result": {
"name": "result",
"override-type": "any"
}
}
}
}
ここについて今の TSJS-lib-generator を確認したところ、書き方が変わっているようでした。
さらに、今回新しく FileReaderProgressEvent
を追加するので inputfiles/addedTypes.json
に以下の内容を追加します。
{
"interfaces": {
"interface": {
// ...
"FileReaderProgressEvent": {
"name": "FileReaderProgressEvent",
"extends": "ProgressEvent",
"properties": {
"property": {
"name": {
"name": "target",
"read-only": 1,
"override-type": "FileReader | null"
}
}
},
"no-interface-object": "1"
},
// ...
}
}
}
name
で追加する型、properties
でその型が持つプロパティを指定しているなど、このファイルについてこれといって定義はないのですが、意味は推測できると思います。ひとつ注意したいのは "no-interface-object": "1"
です。これは PR を出したあとにレビューで指摘されて追加したのですが、これがないと余計なオブジェクトが宣言されてしまうようです。
3. 新しい dom.generated.d.ts と webworker.generated.d.ts を作る
型を定義する json を書けたので、ビルドスクリプトを実行して dom.generated.d.ts と webworker.generated.d.ts を作ります。ビルドスクリプトというのは README にある通り npm run build
で実行できます。
生成した型定義ファイルに問題があれば json を調整します。それらしい型定義ができたら次のステップに進みます。
4. ベースラインを更新する
TSJS-lib-generator では、生成された型定義ファイルをベースラインの型定義と比較することで意図しない変更を検出するようになっています。ここでは型定義ファイルを書き換えたいので npm run baseline-accept
を実行します。これで、生成した型定義ファイルがベースラインに反映されます。
PR を出して accept してもらう
ここまでのステップをたどることで TSJS-lib-generator に PR を出せる状態になりました!それらしい内容を書いて PR を出してみます。私が Microsoft/TSJS-lib-generator#398 を出した時はかなりシンプルな内容で出しました。
PR を出すと Microsoft の bot が CLA にサインするように求めてくるので、サインします。
ここまで済めば、レビュワーの方がなにかしらレビューしてくれるはずです。内容に問題がなければ accept されて、そのうち TypeScript 自体に取り込まれることでしょう。
まとめ
TypeScript の型定義に手を入れる方法を説明してきました。仕組みが分かれば誰でも触れそうだなというのが伝われば幸いです。
オチってわけではないのですが、実はこの FileReader.onload
に対する変更は、TypeScript 3.0 でワケあって取り消されています。便利だったと思うんですけどね。
11日目は tsukakei さんです。よろしくお願いします!