TypeScript 1.6時代の.d.ts管理について意見を述べておく


おはようございますわかめです。
TypeScript 1.6で[node_modulesからの依存性解決](https://github.com/Microsoft/TypeScript/issues/2338)が入りました。
まだ[.d.tsより.tsが優先してlookupされる不具合](https://github.com/Microsoft/TypeScript/issues/4667)などがありますが、実質上、型定義ファイルを自然な形でバンドルできるようになったのです。
npm installして使えば型定義が有効になっている。暖かいですね。

過渡期の機能の常として、複数のやり方がありいくつかのワークアラウンドが必要な場合があります。

ですので、ここではわかめが考える現在最も便利な運用について意見を述べていきます。

## TL;DR

* ライブラリ作成側は
	* 自力で.d.tsをメンテできる場合のみnpmパッケージに.d.tsをバンドルする
		* 非TypeScriptユーザかつTypeScriptの型について詳しくなければ今のままDefinitelyTypedに丸投げのほうがよい
		* .ts はnpm packageに含めない
	* package rootにindex.d.tsを置くか、package.jsonのtypingsにエントリーポイントとなる型定義ファイル名を書く
	* `declare module "hoge"` を使わず、1 .jsファイル = 1 .d.tsファイル の形式を利用する
		* 要するに `tsc --declaration` での出力をそのまま使うと良い
	* 自分のライブラリに関する型定義のみ提供する
		* 依存するライブラリの型定義の面倒は明示的には見ない
* ライブラリ利用側は
	* npmパッケージに型定義ファイルがバンドルされているか調べる
		* index.d.tsの存在チェックかpackage.jsonのtypingsプロパティ
	* 不足する型定義ファイルをtsdやdtsmを利用して補う
		* 多くの場合孫レベルを含めた面倒を見ないとならない
	* 何が不足しているかはコンパイルしてみて、エラーメッセージから判別する(つらい

## 機能の解説

TypeScript 1.6から、 `node_modules` からのライブラリの読み込みが可能になりました。
今まで、TypeScriptは `--module commonjs` をサポートしてはいましたがNode.js式のライブラリの解決はできませんでした。

TypeScript 1.6からはそこが改善され、 `--module commonjs` を指定した場合か、`--moduleResolution node` を指定した場合、 `import * as hoge from "hoge";` のようなコードに対して `node_modules/hoge` からモジュールを発見してくれるようになります。
これにあわせてnpmパッケージにバンドルされている.d.tsファイルが自動的に検出されるようになったため、dtsmやtsdを使ってDefinitelyTypedのリポジトリから別途型定義ファイルを持ってくる手間が不要になります。
同時に、DefinitelyTypedではライブラリの最新のバージョンに対してのみ型定義ファイルをメンテする方針ですが、npmパッケージにバンドルされることにより利用するバージョンに対応した型定義ファイルが利用できるようになるため、自分のペースで依存関係のバージョンを更新することがより容易になります。
逆にいうと、自分のコードにマッチした型定義ファイルをメンテナンスできないのであれば.d.tsをバンドルするべきではありません。(これはTypeScriptでライブラリを書いている人には無用の心配ですね)

この方法が広く利用されるようになれば、同一ライブラリのメジャーバージョン違いが依存先に混在していた場合にも上手くコンパイルできるようになるでしょう。

### モジュール名解決のルール

[Issueに書かれた擬似コード](https://github.com/Microsoft/TypeScript/issues/2338#issue-60991541)によると、モジュール検出のルールは概ね以下のとおりです。

ある、 `hoge` や `./hoge` というモジュールの解決について…

1. ambientな( `declare module "hoge"` 形式な )モジュールの定義が存在する場合それを使う
2. `./` や `../` でモジュール名が始まる場合、同名のファイルやディレクトリがあるかを探す
	* 今までと違い `./hoge/index.ts` は `./hoge` にマッチするようになった。もちろん `./hoge.ts` にもマッチする。`./hoge.d.ts` や `./hoge.tsx` もマッチする。
3. `hoge/package.json` が存在する場合、package.jsonの情報を参照しようと試みる
	1. `typings` プロパティがあれば、そこに書いてある型定義ファイルを参照する。例えば `hoge/package.json` が `{"typings": "./lib/hoge.d.ts"}` だった場合、`hoge/lib/hoge.d.ts` が参照される。
4. `hoge/index` が参照される。優先度は.d.tsのみ読み込む場合は `index.d.ts` 、それ以外の場合は `index.ts` > `index.tsx` > `index.d.ts` となる。

### 制限

* `declare module "hoge"` 形式の定義は利用できない
	* `error TS2306: File 'node_modules/fuga/fuga.d.ts' is not a module.`
	* = DefinitelyTyped上の自分のライブラリの型定義ファイルをコピーしてきてもそのままでは動作しない
* reference comment (`/// <reference path="./fuga.d.ts" />`) は利用できない
	* `error TS2654: Exported external package typings file cannot contain tripleslash references. Please contact the package author to update the package definition.`
	* コンパイル時にだけ参照したい型定義ファイルが読み込まれるようにする。そのためにtsconfig.jsonを利用する ([参考](https://github.com/vvakame/commandpost/blob/master/tsconfig.json#L36-L41))
	* さもなくば、reference commentを集めたコンパイル時entry pointな.tsファイルを用意してもいいかも オススメしないけど

### 必要なワークアラウンド

* TypeScriptコンパイル時のオプション基本コンボは `--declaration --module commonjs --sourceMap --inlineSources`
* npm publishする時に .ts は出荷しない
	* 理由：[.tsが意図せず利用側で再コンパイルされちゃうから](https://github.com/Microsoft/TypeScript/issues/4667#issuecomment-141288373)
	* .gitignoreで .js と .js.map と .d.ts はignoreする
	* .npmignoreで .ts はignore する
* `--sourceMap` を利用する場合、 `--inlineSources` を併用する
	* npmで.tsをpublishしないので、.js.map内に.tsのデータ自体を持たないとスタックトレースが見難くなったりする
* リファレンスコメント (`/// <reference path="./typings/jquery/jquery.d.ts" />`) を利用しない
	* tsconfig.jsonに依存する型定義ファイルの参照を押し込み、tsconfig.json経由でコンパイルする

### まだ残ってる問題

* ライブラリが依存しているライブラリ(大本の利用側から見ると孫ライブラリ)が要求する型定義ファイルの解決を大本の利用側で解決して整合性を取ってやらねばならない。
	* なので、型定義ファイルを必要とする孫となるライブラリの数はなるべく絞ったほうが良い
		* 孫ファイルが.d.tsをバンドルしていて型定義ファイルを明示的に補わなくて良い場合は気にする必要はない
	* もしくは、孫となるライブラリがexportしている型をなるべくライブラリ側でexportしないようにする
		* 型をexportしなければ利用側では孫ライブラリの型を気にしなくてよくなる

## 利用する側に求められること

1. 利用するライブラリが型定義を持っているかを調べる
	* index.d.ts か package.json に typings を持っているか
2. 持っていない場合、dtsmやtsdを使ったりして型定義ファイルを入手してくる
3. ライブラリを利用してコンパイルするなどしてみて、型が不足する旨のエラーが出るかを確かめる
4. 不足がある場合、dtsmやtsdを使ったりして型定義ファイルを入手してくる

これだけです。今までは2番だけ行えばOKでしたね。環境構築の時点では手間が若干増えています。
型定義ファイルがバンドルされている場合でも、`declare module "hoge"` 形式の型定義ファイルが先に読み込まれている場合そちらが利用されてしまうので注意が必要です。

## よくある質問

Q. DefinitelyTypedに僕のライブラリの型定義があるんだけど、あれを持ってきても動くの？
A. 動かないよ！ambient module declarationは許可されないよ！ `error TS2306: File 'node_modules/fuga/index.d.ts' is not a module.` とか言われちゃうよ！

Q. TypeScriptの型定義ファイルって結構Breaking Change入るの？安定してる？
A. ちょいちょいあります。しかし、それは"より強固なチェック"のためのBreaking Changeで、文法の変更によるものではないはずです。
つまり、適切な型定義ファイルを最初から作れていればあまり気にする必要はないでしょう。

Q. tsconfig.json使わないとダメ？
A. 使わなくてもいいけど、IDEが参照してくれたりして楽なので作ったほうがいいと思うよ。 `tsc -p ./` でコンパイルできるプロジェクトは健全だと思うよ。

Q. DefinitelyTypedでよくね？バンドルする利点何かあるの？
A. バンドルする場合、インストールしたバージョンに適した型定義ファイルが必ず手に入るという利点があります。
DefinitelyTypedではあるライブラリの最新のバージョンの定義しかメンテナンスを頑張らない方針です。つまり、DefinitelyTyped上の型定義ファイルは新しいバージョンに進んでいってしまうので、古いバージョンの型定義でうまく欲しいものに合致するものを探すのは大変です。
バンドルすればその心配はありません。パッケージについてくる型定義ファイルを常に使えば良いのです。

Q. 何かサンプルある？サンプル！サンプル！
A. [commandpost](https://github.com/vvakame/commandpost)のtsconfig.jsonと.gitignoreと.npmignore見ればいいよ！commandpostの利用例は[prh](https://github.com/vvakame/prh)とかがあるよ！

Q. どうやって上手く読み込まれているかテストすればいいの？
A. `--listFiles` オプションていうのがある。利用側コードを書いてみて、`--listFiles` 付きでコンパイルしてみて `node_modules/hoge/index.d.ts` みたいな感じで自分がリリースした型定義ファイルがリストに現れればOK。

追加で質問があればコメントかTwitterで教えてね！