要約
npmからインストールしたパッケージに型定義ファイル (*.d.ts) が存在しない場合、独自の型定義ファイルを作り、下記1, 2のどちらかの設定を行う事で、import時に型定義の内容を適用させることができます。
- TypeScriptオプションの
baseUrlとpathsの組み合わせで型定義ファイルのパスを指定する - 型定義ファイルの中で、
declare module "xxx" { ... }で外部モジュールのアンビエント宣言を行う
TypeScriptオプションのtypeRootsでもこれを実現できると紹介されている場合がありますが、typeRootsは import時には効かない ため、実際には上記の目的には使用できません。
(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通りの方法が広まっています。
-
TypeScriptオプションの
typeRootsで型定義ファイルを置いているディレクトリを指定するtsconfig.json{ "compilerOptions": { "typeRoots": ["node_modules/@types", "src/@types"], // src/@types ディレクトリの中にある型定義を読み込ませたい ... } } -
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さんの記事)
-
任意の型定義ファイルの中で、外部モジュール
mod1のアンビエント宣言を行うmytypes/mod1.d.tsdeclare 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" />
ですがこの指定は、 baseUrlとpathsの組み合わせでほぼ代替可能 であり、またこちらの組み合わせであればimport時にも型定義ファイルの内容を反映することができるため、より幅広いケースに対応することができます。
このため、baseUrlとpathsが実装されてから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が反映されないことを確認できます。
-
新しいディレクトリ内で下記のコマンドを実行し、TypeScriptのビルド環境を整える
% yarn add typescript % yarn tsc --init # 推奨設定でtsconfig.jsonを初期化 (strictオプションも有効になる) -
pokemon-names-and-types パッケージをインストールする(このパッケージには型定義ファイルが存在しない)
% yarn add pokemon-names-and-types -
下記のような
main.tsファイルを作成main.tsimport { pkmn } from 'pokemon-names-and-types' console.log("Go! %s!", pkmn.random()); -
同じディレクトリ内に、新しく
typesディレクトリとtypes/pokemon-names-and-typesディレクトリを作成し、その下にindex.d.tsを作成
(package.jsonが存在しない場合、TypeScriptの場所解決ルールによりindex.d.tsが標準で読み込まれるはず)types/pokemon-names-and-types/index.d.tsinterface PokemonObj { random: () => string; } export var pkmn: PokemonObj; -
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. */ -
ビルドを実行 →
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時には効かない」という事を伝えるためだけの記事のはずが、思った以上に長くなってしまいました![]()
この記事を読んで、TypeScriptが型定義ファイルを検索する仕組みやtypeRoots, baseUrl, pathsの役割について、少しでも理解を深めてもらうことができれば幸いです。(私自身、このあたりの仕組みがよく分からず苦労したので……)
もしも意見や感想、誤りの指摘、あるいは「記事のここがわかりにくいので加筆してほしい」といった要望があれば、本記事のコメントやTwitterからメッセージを寄せていただけると嬉しいです。
なお、「import時にどのような手順で型定義ファイルの検索を行っているのか」について知りたい方は、下記リンク先に詳細に記述されているため、こちらをご参照ください。私もこの記事を書くにあたり、下記ページの内容がたいへん参考になりました。
- TypeScript Handbook を読む (16. Module Resolution) (公式Handbookの @murank さんによる日本語訳)
- TypeScript Deep Dive 日本語版 - File Module Details (Yohamataさんを含む有志によるTypeScript Deep Diveの日本語訳)