TypeScript の FileReader に手を入れた話

TypeScript 2.7 で `FileReader` を使うコードを書くときに型情報が不足しているのが気になったので [Microsoft/TSJS-lib-generator](https://github.com/Microsoft/TSJS-lib-generator) に [PR #398](https://github.com/Microsoft/TSJS-lib-generator/pull/398) を出して TypeScript 2.9 に取り込んでもらった話です。

事の発端は TypeScript 2.7 で書いていたこのようなコードです。

```ts
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](https://developer.mozilla.org/ja/docs/Web/API/FileReader/onload) にあるようなシンプルなファイル読み込みなのですが、これを書くと「`event.target.result` が無いよ」と言われてしまいました。

同じような問題を訴えている [TypeScript の issue](https://github.com/Microsoft/TypeScript/issues/4163) もあったので、よっしゃ黙らせたるってことで、TypeScript 自体の FileReader に手を入れてみることにしました。

## どこに手を入れればいいのか探す

`FileReader.onload` は `node_modules/typescript/lib/lib.dom.d.ts` でこのように定義されています。

```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` と書いても何も言われなくなります。イメージとしてはこんな形を望んでいます。

```ts
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](https://github.com/Microsoft/TypeScript) にある Contribute という項目からは [CONTRIBUTING.md](https://github.com/Microsoft/TypeScript/blob/master/CONTRIBUTING.md) へのリンクが貼られています。ここに書いてあることに従っておけばよさそうです。

この資料の中を lib で検索すると、すぐに [Contributing lib.d.ts fixes](https://github.com/Microsoft/TypeScript/blob/master/CONTRIBUTING.md#contributing-libdts-fixes) という項目に引っかかるはずです。そこには

- ライブラリのソースファイルは `src/lib` にあるよ
- `src/lib/dom.generated.d.ts` は自動生成されるので、変更したい場合は https://github.com/Microsoft/TSJS-lib-generator に PR を出してね

と書いてあります。[`lib/README.md`](https://github.com/Microsoft/TypeScript/blob/master/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](https://github.com/Microsoft/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 環境で使えるかを決めます。

それぞれのファイルのフォーマットは内容から察してください。

プルリクエストを送る一般的なステップは:

0. [TypeScript リポジトリ](https://github.com/Microsoft/TypeScript) の issue を作るか参照する
1. 足りない要素を `inputfiles/addedTypes.json` に追加したり、`inputfiles/overridingTypes.json` で要素を上書きしたり、`inputfiles/removedTypes.json` で要素を削除する
2. ローカルでビルドスクリプトを実行して、新しい `dom.generated.d.ts` と `webworker.generated.d.ts` を作る
3. `generated` フォルダに出力されたファイルを使って `baselines` フォルダのファイルを更新する (`npm run baseline-accept`)

（訳注：原文ではステップ 0 から始まるのですが Qiita だと 1 に書き換えられてしまうようです）

----

以上、和訳でした。というわけで、このステップをたどって `FileReader.onload` の引数の型を書き換えていきます。

## 1. TypeScript の issue を作るか参照する

書いてある通りです。この時は [FileReader.onLoad/onLoadEnd event argument has no target.result property](https://github.com/Microsoft/TypeScript/issues/4163) という issue があったので、これを参照する形にしました。

## 2. json ファイルを編集する

`FileReader.onload` の型を変えるために `inputfiles/overridingTypes.json` を編集します。`inputfiles/overridingTypes.json` の中を探すと `FileReader` についてこのように書かれています。

```inputfiles/overridingTypes.json
            "FileReader": {
                "name": "FileReader",
                "properties": {
                    "property": {
                        "result": {
                            "name": "result",
                            "override-type": "any"
                        }
                     }
                }
            },
```

今回は `onload` を `(this: FileReader, ev: FileReaderProgressEvent) => any` にしたいので、このように書きます。

```inputfiles/overridingTypes.json
            "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` に以下の内容を追加します。

```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](https://github.com/Microsoft/TSJS-lib-generator/pull/398) を出した時はかなりシンプルな内容で出しました。

PR を出すと Microsoft の bot が CLA にサインするように求めてくるので、サインします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/6870/0d89b582-a2d3-27dc-62cc-8c4403e82713.png)

ここまで済めば、レビュワーの方がなにかしらレビューしてくれるはずです。内容に問題がなければ accept されて、そのうち TypeScript 自体に取り込まれることでしょう。

## まとめ

TypeScript の型定義に手を入れる方法を説明してきました。仕組みが分かれば誰でも触れそうだなというのが伝われば幸いです。

オチってわけではないのですが、実はこの `FileReader.onload` に対する変更は、TypeScript 3.0 で[ワケあって](https://github.com/Microsoft/TypeScript/issues/25510)取り消されています。便利だったと思うんですけどね。

11日目は [tsukakei](https://qiita.com/tsukakei) さんです。よろしくお願いします！