LoginSignup
196
111

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-18

要約

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

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

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

解説

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

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

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で型定義ファイルを置いているディレクトリを指定する

    tsconfig.json
    {
      "compilerOptions": {
        "typeRoots": ["node_modules/@types", "src/@types"],   // src/@types ディレクトリの中にある型定義を読み込ませたい
        ...
      }
    }
    
  2. TypeScriptオプションのbaseUrl, pathsの組み合わせで型定義ファイルへのパスを指定する

    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について (@Quramyさんの記事)

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

    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時に反映されるわけではなく、あくまでビルド対象に含めるだけです)。

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

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

参考:typeRoots is not resolved as part of compilation #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のビルド環境を整える

    % yarn add typescript
    % yarn tsc --init   # 推奨設定でtsconfig.jsonを初期化 (strictオプションも有効になる)
    
  2. pokemon-names-and-types パッケージをインストールする(このパッケージには型定義ファイルが存在しない)

    % yarn add pokemon-names-and-types
    
  3. 下記のようなmain.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の場所解決ルールによりindex.d.tsが標準で読み込まれるはず)

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

    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からメッセージを寄せていただけると嬉しいです。

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

196
111
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
196
111