typeRootsの誤解 -- TypeScriptで、npmからインストールしたパッケージに型定義ファイル (*.d.ts) が存在しない場合の正しい対処方法


# 要約
npmからインストールしたパッケージに型定義ファイル (`*.d.ts`) が存在しない場合、独自の型定義ファイルを作り、下記1, 2のどちらかの設定を行う事で、import時に型定義の内容を適用させることができます。

1. TypeScriptオプションの`baseUrl`と`paths`の組み合わせで型定義ファイルのパスを指定する
2. 型定義ファイルの中で、`declare module "xxx" { ... }` で外部モジュールのアンビエント宣言を行う

TypeScriptオプションの`typeRoots`でもこれを実現できると紹介されている場合がありますが、`typeRoots`は __import時には効かない__ ため、実際には上記の目的には使用できません。
（importは`typeRoots`で指定されたディレクトリ内の型定義ファイルを探しません）


# 解説
TypeScriptでプログラムを書いているときに、npmにあるパッケージをインストールしていると、 __パッケージの型定義ファイル (`*.d.ts`) が提供されていない（`@types/xxx`パッケージも存在しない）__ という場合があります。

この場合、そのパッケージをimportしようとしても型定義ファイルが見つからないために、`strict`オプション（もしくは`noImplicitAny`オプション）が有効な環境ではビルドエラーとなってしまいます。
また、上記以外の環境ではビルドエラーとはならないものの、importしたオブジェクトや関数が`any`型となってしまうために、存在しない関数や定数を呼びだそうとしてもエラーにならず、TypeScriptの型チェックの利点を十分に活かすことができません。

```tsx
import * as mod1 from "mod1";

// mod1パッケージをnpmからインストールしていたとしても、型定義ファイルが存在しなければ、strict環境ではビルドエラーとなる
//
// エラーの内容は下記:
// モジュール 'mod1' の宣言ファイルが見つかりませんでした。'/home/.../node_modules/mod1/index.js' は暗黙的に 'any' 型になります。
// Try `npm install @types/mod1` if it exists or add a new declaration (.d.ts) file containing `declare module 'mod1';`ts(7016)
```

このような場合に、独自で`mod1`モジュールの型定義ファイル (`mod1.d.ts`) を作り、その型定義の内容をimport時に反映する方法として、大きく下記の3通りの方法が広まっています。

1. TypeScriptオプションの`typeRoots`で型定義ファイルを置いているディレクトリを指定する

    ```js:tsconfig.json
    {
      "compilerOptions": {
        "typeRoots": ["node_modules/@types", "src/@types"],   // src/@types ディレクトリの中にある型定義を読み込ませたい
        ...
      }
    }
    ```

2. TypeScriptオプションの`baseUrl`, `paths`の組み合わせで型定義ファイルへのパスを指定する

    ```js:tsconfig.json
    {
      "compilerOptions": {
        "baseUrl": "src/typings" ,   // src/typings ディレクトリを起点とする (pathsの指定時は必須)
        "paths": {
          "mod1": ["mod1.d.ts"] // "mod1" のimport時に src/typings/mod1.d.ts を読み込ませる
                                // ※拡張子は省略可能のため、"mod1" や "mod1.d" も可
                                // ※このパス指定は省略可能で、省略した場合は自動的に src/typings/mod1.d.ts や src/typings/mod1/index.d.ts などを検索する
        },
        ...
      }
    }
    ```

    参考：[TypeScript 2.0のModule Resolution Enhancementsについて](https://qiita.com/Quramy/items/44ab1a046d58449cd783) (@Quramyさんの記事)

3. 任意の型定義ファイルの中で、外部モジュール`mod1`のアンビエント宣言を行う

    ```ts:mytypes/mod1.d.ts
    declare module "mod1" { // 外部モジュール「mod1」として宣言
        export function func1(): void;
        export function func2(): void;
    }
    ```

    ※この「任意の型定義ファイル」は、`tsconfig.json` と同階層以下であればどの場所に置いてもよい


ですが、この1, 2, 3のうち1の「`typeRoots`で型定義ファイルを置いているディレクトリを指定する」方法は、実は __import時に反映させる方法としては正しくありません。__
`typeRoots`で型定義ファイルのパスを指定したとしても、実際にはimportの結果はビルドエラーもしくは`any`となります。

※上記を確認するための手順やエラーの内容については、後述の[検証手順](#検証手順)を参照してください


## そもそも`typeRoots`とは何か

`typeRoots`は __トリプルスラッシュディレクティブでの参照時のみ効果を及ぼすオプション__ です。

たとえば、下記のような形で`mod1`を参照すると、`typeRoots`に指定したディレクトリ以下のモジュール`mod1`にある定義ファイルをビルド対象に含めることができます（import時に反映されるわけではなく、あくまでビルド対象に含めるだけです）。

```ts
/// <reference types="mod1" />
```

ですがこの指定は、 __`baseUrl`と`paths`の組み合わせでほぼ代替可能__ であり、またこちらの組み合わせであればimport時にも型定義ファイルの内容を反映することができるため、より幅広いケースに対応することができます。
このため、`baseUrl`と`paths`が実装されてから3年ほど経った今となっては、上記`/// <reference types="mod1" />`も`typeRoots`も使用する必要がないのではないかと思います。（tslintでも`/// <reference ... />`は[修正対象として検出されます](https://palantir.github.io/tslint/rules/no-reference/)）

参考：[typeRoots is not resolved as part of compilation #27026](https://github.com/Microsoft/TypeScript/issues/27026) （TypeScript公式レポジトリのIssue）



## なぜ実際には効果のない`typeRoots`を使う方法が広まったのか？

正確な原因は分かりませんが、おそらくは下記のような理由によるものではないかと思います。

- `strict`（もしくは`noImplicitAny`）オプションが有効でない場合、型定義ファイルの内容が反映されていなかったとしても、`any`にはなるがビルドエラーにはならない。このため実際に試したときに、型定義ファイルの内容が反映されないことに気づきにくい
- `typeRoots`の指定と`declare module "xxx" { ... }`によるモジュールのアンビエント宣言を組み合わせていると、あたかも`typeRoots`のおかげで型定義ファイルが読み込まれているかのように見える可能性がある
- TypeScriptの型定義の検索やimportの仕組み自体が複雑であるため、理解しづらい
- もしかすると以前のバージョンのTypeScriptでは効果があった？（未確認）

実際、私もTypeScriptを使い始めてから1～2年ほどになりますが、 __数日前までこの事実に気づかないままずっと`typeRoots`オプションを使い続けていました。__ 新しいコードを書くときに`strict`オプションを有効にしていなければ、今も気づかないままだったことでしょう。


# 検証手順

node.jsとyarnがインストールされている環境で、下記の手順を実行すると、import時に`typeRoots`が反映されないことを確認できます。

1. 新しいディレクトリ内で下記のコマンドを実行し、TypeScriptのビルド環境を整える

    ```sh
    % yarn add typescript
    % yarn tsc --init   # 推奨設定でtsconfig.jsonを初期化 (strictオプションも有効になる)
    ```

2. [pokemon-names-and-types](https://github.com/SahajR/pokemon-names-and-types) パッケージをインストールする（このパッケージには型定義ファイルが存在しない）

    ```sh
    % yarn add pokemon-names-and-types
    ```

3. 下記のような`main.ts`ファイルを作成

    ```ts:main.ts
    import { pkmn } from 'pokemon-names-and-types'
    
    console.log("Go! %s!", pkmn.random());
    ```

4. 同じディレクトリ内に、新しく`types`ディレクトリと `types/pokemon-names-and-types` ディレクトリを作成し、その下に `index.d.ts` を作成
（`package.json`が存在しない場合、[TypeScriptの場所解決ルール](https://typescript-jp.gitbook.io/deep-dive/project/modules/external-modules#placetohaka)により`index.d.ts`が標準で読み込まれるはず）

    ```ts:types/pokemon-names-and-types/index.d.ts
    interface PokemonObj {
        random: () => string;
    }
    export var pkmn: PokemonObj;
    ```
5. `tsconfig.json`内の`typeRoots`オプションを設定

    ```js:tsconfig.json
        // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
        "typeRoots": ["./node_modules/@types", "./types"],                       /* List of folders to include type definitions from. */
        // "types": [],                           /* Type declaration files to be included in compilation. */

    ```

6. ビルドを実行 → __`pokemon-names-and-types` の型定義ファイルが見つけられずにビルドエラーが発生__

    ```
    % yarn tsc
    $ /home/user1/dir1/node_modules/.bin/tsc
    main.ts:1:22 - error TS7016: Could not find a declaration file for module 'pokemon-names-and-types'. 
    '/home/user1/dir1/node_modules/pokemon-names-and-types/dist/main.js' implicitly has an 'any' type.
      Try `npm install @types/pokemon-names-and-types` if it exists or add a new declaration (.d.ts) file containing `declare module 'pokemon-names-and-types';`

    1 import { pkmn } from 'pokemon-names-and-types'
                           ~~~~~~~~~~~~~~~~~~~~~~~~~


    Found 1 error.
    ```




# おわりに

「`typeRoots`はimport時には効かない」という事を伝えるためだけの記事のはずが、思った以上に長くなってしまいました:sweat_smile:

この記事を読んで、TypeScriptが型定義ファイルを検索する仕組みや`typeRoots`, `baseUrl`, `paths`の役割について、少しでも理解を深めてもらうことができれば幸いです。（私自身、このあたりの仕組みがよく分からず苦労したので……）
もしも意見や感想、誤りの指摘、あるいは「記事のここがわかりにくいので加筆してほしい」といった要望があれば、本記事のコメントや[Twitter](https://twitter.com/tetradice)からメッセージを寄せていただけると嬉しいです。

なお、「import時にどのような手順で型定義ファイルの検索を行っているのか」について知りたい方は、下記リンク先に詳細に記述されているため、こちらをご参照ください。私もこの記事を書くにあたり、下記ページの内容がたいへん参考になりました。

- [TypeScript Handbook を読む (16. Module Resolution)](https://qiita.com/murank/items/5f11466474d49498bbd5) （公式Handbookの @murank さんによる日本語訳）
- [TypeScript Deep Dive 日本語版 - File Module Details](https://typescript-jp.gitbook.io/deep-dive/project/modules/external-modules)　（Yohamataさんを含む有志によるTypeScript Deep Diveの日本語訳）