TypeScriptプロジェクトに独自の型定義を配置する方法

## 概要

TypeScriptの開発において、型定義ファイルが用意されてないnpmライブラリを使うのは厄介なものです。
無理やり `require("...")` でJSのまま読み込ませてもいいですが、ちゃんとした型定義を使用したくなるときもあります。

作成した型定義ファイルは元のnpmライブラリに取り込んでもらい `package.json` の `types` で指定してもらう方法 [^types] と、 [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/) に取り込んでもらい `@types/***` パッケージとして`npm install`する方法がありますが、どちらも取り込んでもらうためにはPull Requestを投げたり、ライブラリのメンテナに新しいバージョンとしてリリースしてもらったりで時間がかかってしまいます。

Pull Requestは投げつつ、それが取り込まれてリリースされるまでの間、自分のプロジェクトで作った型定義ファイルを利用できる方法はないものかと思って調べてたんですが、なかなかちょっと面倒くさかったのでここにまとめておこうと思います。

## 実行環境

- Node.js 10.5.0
- TypeScript 2.9.2

## やってみる

今回はDefinitelyTypedのREADMEでもいい型定義ファイルの例としてなぜか推奨されている [base64-js](https://www.npmjs.com/package/base64-js) を利用します。 [^base64js]

[^base64js]: 型定義を新たに作ってみようと思うときに参考にしようとしてもbase64-jsはシンプルすぎて役に立たないことのほうが多いと思います。関数しかないライブラリなら参考になりますけど。

> For a good example package, see base64-js.
> https://github.com/DefinitelyTyped/DefinitelyTyped#create-a-new-package

### base64-js の型定義

base64-jsは公開されている関数が3つしかないため、[型定義ファイル](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/base64-js/index.d.ts) も次のようなシンプルなものです。

```typescript:base64-js/index.d.ts
// Type definitions for base64-js 1.2
// Project: https://github.com/beatgammit/base64-js
// Definitions by: Peter Safranek <https://github.com/pe8ter>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped

export function byteLength(encoded: string): number;
export function toByteArray(encoded: string): Uint8Array;
export function fromByteArray(bytes: Uint8Array): string;
```

今回はこのライブラリを利用した簡単なソースコードで実験を行います。

```typescript:index.ts
import { toByteArray } from "base64-js";
import { TextDecoder } from "util";

const encoded = "44GT44KT44Gr44Gh44Gv"; // "こんにちは" をBase64エンコードしたもの
console.log(`encoded: ${encoded}`);
const decoded = new TextDecoder().decode(toByteArray(encoded));
console.log(`decoded: ${decoded}`);
```

### 設定方法

今回は独自の型定義ファイルを配置する方法を確認するため、npmの [@types/base64-js](https://www.npmjs.com/package/@types/base64-js) は利用せず、この型定義ファイルをプロジェクト内に配置することにします。

<img width="1107" alt="structure.png" src="https://qiita-image-store.s3.amazonaws.com/0/8846/adbe87c4-dafc-c4e2-edba-62f7901d9c40.png">

プロジェクト直下に `types` ディレクトリを作成し、`/types/base64-js/index.d.ts` に先ほどの型定義を配置しましたが、このままではコンパイルすることはできません。
`tsc` が型定義ファイルの位置を知らないためです。

```console
$ npm run build

> original-definition-in-typescript-project-sample@1.0.0 build /Users/mtgto/work/js/original-definition-in-typescript-project-sample
> tsc --project tsconfig.json

src/index.ts:1:29 - error TS7016: Could not find a declaration file for module 'base64-js'. '/Users/mtgto/work/js/original-definition-in-typescript-project-sample/node_modules/base64-js/index.js' implicitly has an 'any' type.
  Try `npm install @types/base64-js` if it exists or add a new declaration (.d.ts) file containing `declare module 'base64-js';`

1 import { toByteArray } from "base64-js";
                              ~~~~~~~~~~~
```

これをコンパイラに教えてあげるため、 `tsconfig.json` の `typeRoots`, `baseUrl`, `paths` を次のように変更します（ `typeRoots` のデフォルトは `[]` です)。

```json
{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {"base64-js": ["types/base64-js"]},
        "typeRoots": ["types", "node_modules/@types"],
    }
}
```

`paths` によって `import * as base64 from "base64-js"` のように書くことが許されるようになります。
`baseUrl` は `paths` を使う際に設定する必要があります。ソース内で相対パスではなくimportしたいときなんかは `src` を指定したりすることがあるようです。 [^baseurl]
また `paths` は `"paths": {"*": ["types/*"]}, ` のように書くことで複数の独自型定義をまとめて設定することもできます。

`typeRoots`  [^typeroots] は実は今回のケースでは書かなくてもコンパイルできます。もうすでに独自の  `typeRoots` を指定しており、新たに今回の `/types` も使用したい場合に追加する必要があります。

すでに型定義ファイルをもっているライブラリを上書きする形でしたいときに独自型定義を使いたいときに設定するのかなとおもって `@types/base64-js` をインストールした上で `typeRoots` から `types` ディレクトリを消してみたりしたのですが、ちゃんと `types/base64-js/index.d.ts` にしかない関数を呼んだコードをコンパイルすることができました。なのでライブラリの型定義ファイルが古くてちょっとだけ自分のプロジェクト内だけでも型定義を書き換えたいときに使ったりできそうです。

各項目について詳しくは[公式のtsconfig.jsonの説明](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) も参考にしてください。

## 実行してみる

無事に独自型定義でコンパイルが通ったので実行してみます。

```console
$ node .
encoded: 44GT44KT44Gr44Gh44Gv
decoded: こんにちは
```

動きました。以上です。

## サンプルコード

今回の記事で紹介したコードを含むプロジェクトを↓においています。

https://github.com/mtgto/original-definition-in-typescript-project-sample

## 補足

試行錯誤してたときにだめだったことを書いておきます。

### よくない例: 独自型定義ファイルを `*.ts` から相対パスで参照する

コンパイルは通りますが、JSが参照するライブラリが相対パス表記になってしまい、実行時に「ライブラリが見つからない」とnodeに怒られました。
コンパイル後に `.js` ファイル内の相対パスを書き換える強引な手もありますが、SourceMapとの関連がずれることもあるでしょうし、やはりおすすめできません。

### おすすめしない例: 独自型定義ファイルを `node_modules/@types` に配置する

`.gitignore` で node_modules 以下を管理していることは多いのでおすすめできません。

# まとめ

- TypeScriptの独自型定義を置くときには `tsconfig.json` の `baseUrl`, `paths` に絶対パスで指定できるように設定をしましょう
  - `typeRoots` を使っているときは `typeRoots` にも追加しましょう
- もしよければライブラリかDefinitelyTypedにPRを送り、他の人に貢献しましょう！

[^types]: https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#including-declarations-in-your-npm-package
[^baseurl]: 例えば https://stackoverflow.com/a/43330003
[^typeroots]: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types