要約
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の日本語訳)