この記事は以下のような需要に応えようとするものです。
- 他人のNPMパッケージを自分のTypeScriptプロジェクトで使いたいが、型情報が付いていない
- Definitely Typedを探しても型情報がない
- 自分のパッケージ内で使う部分だけ型情報を付けたい
- 依存パッケージ全体に型情報を書いてDefinitely Typedにコミットするほどのリソースがない
こうした需要に対して、*.d.ts
の書き方(アンビエント宣言の文法など)についての解説はすぐにたくさん見つかりますが、型定義ファイルをプロジェクトの中でどう扱うか、tscやVS Codeに型情報を認識させるにはどうしたらいいかといった周辺的な情報を見つけるのに少し苦労したので、そのあたりをフォローしています。
2022年時点でパッケージ内にもDefinitely Typedにも型情報がないパッケージをTypeScriptプロジェクトで使うのはなるべく避けたいところですが、諸事情でやむを得ないこともあり、バッドノウハウのようなものかもしれません。
ただ、自分用から育ててあわよくば型情報を切り出してパッケージ本体やDefinitely Typedへの貢献を狙うこともできるでしょう。
アンビエント宣言とは
TypeScriptには、JavaScriptソースと型情報を別々に提供するための、アンビエント宣言(ambient declaration)という仕組みがあります。
アンビエント宣言を記述したファイルには*.d.ts
という拡張子が使われ、*.js
ファイルとは別に用意されます。
型情報を書くパッケージ
この記事ではis-positive-integerという、関数を2つだけエクスポートしているパッケージを利用します1。
発展としてもう少し複雑な例を練習したくなったら、Definitely Typedに登録されているパッケージのリストからちょうどよいものを探したり、自分でJSを書いてもよいと思います。
下準備
この記事用に「自分のプロジェクト」と想定するNPMパッケージを作っていきます。説明のため色々省くので実用的ではありません。
is-positive-integer
に依存し、関数を1つだけCommonJS形式でエクスポートするパッケージとします。
トランスパイルにはtypescript(tsc)だけを使い、rollupなどは使いません。
npm init
mkdir types-for-me
cd types-for-me
npm init
npm install
npm install -D typescript @types/node
npm install is-positive-integer
tsconfig.json
シンプルな設定にとどめます。
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
},
"files": [
"index.ts",
],
}
トランスパイルされた*.js
と*.d.ts
はdist/
ディレクトリの下に出力される設定です。
ファイル数が少ないのでTypeScriptソースをfilesキーで指定していますが、globを使えるincludeキーで"include": ["src/**/*.ts"]
のようにすることもできます。
package.json
package.json
にmain
、types
、scripts.build
を追加します。
{
...
+ "main": "dist/index.js",
+ "types: "dist/index.d.ts",
"scripts": {
+ "build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
}
index.ts
is-positive-integer
をインポートして利用する(以外にほぼ意味のない)TypeScriptソースです。
import isPositiveInteger from 'is-positive-integer'
export function posiOrNega(n: number): string {
return isPositiveInteger(n) ? 'Positive!' : 'Not Positive!';
}
型情報を書く
やっと型情報を書く段階にきました。といっても関数2つなので大したことはありません。型情報ファイルの内容よりも、プロジェクトの設定にそのファイルをどう反映させるかや、パッケージ内でどう扱うかのほうがポイントです。
is-positive-integer.d.ts
型情報ファイルを作ります2。types
というサブディレクトリを作り、types/is-positive-integer.d.ts
とします。
declare module 'is-positive-integer' {
export default function isPositiveInteger(x: number): boolean;
export function isSafePositiveInteger(x: number): boolean;
}
declare module 'is-positive-integer' { ... }
の部分をアンビエントモジュール(ambient module)と呼び、is-positive-integer
の型情報であることを宣言しています。波括弧の内側に関数のシグネチャを書き、is-positive-integer
パッケージに属する関数についての情報だと示しています。もちろん場合によっては関数のほかに定数、クラス、インターフェースなども書けます。
この型情報ファイルをtsconfig.json
に登録し、コンパイラに型情報の位置を伝えます。
{
"compilerOptions": {
...
},
"files": [
"index.ts",
+ "types/is-positive-integer.d.ts",
],
}
なお、compilerOptions.typeRoots
はimport
するモジュールの型情報を示すためのオプションではなく、今回の場合、使ってもコンパイラに型情報は伝わりません(いかにも使えそうなオブション名ですが)。
typeRoots
はグローバルライブラリ3を参照する/// <reference types="something" />
を使ったときに、参照モジュールを探索する場所を指定する設定です。
compilerOptions.typeRoots
については @tetradice さんが以下の記事で詳しくまとめてくださっています。
依存パッケージの型情報ファイルを公開するかどうか
tsconfig.json
のfiles
にtypes/is-positive-integer.d.ts
を追加したことにより、型情報ファイルがビルド時にcompilerOptions.outDir
(今回はdist/
)へコピーされることになります。
もしnpm publish
する場合、他パッケージから参照されるdist
ディレクトリに別パッケージの型情報を残してしまうのは行儀が良くなさそうです。今回はtscだけでトランスパイルしているので、package.json
のscripts.postbuild
に削除するコマンドを書いておくなどが考えられます。
依存しているパッケージのオブジェクトを、自分のバッケージで加工せず、自分のパッケージの利用者へ直接渡すようなケースもあるかもしれませんが、そういう場合は改めて自分のパッケージの型として公開するのがよいと思います。
型定義をテストする
*.d.ts
の型定義のテストを書いていきます。
型定義テスト用のモジュールとしてはSamVerschueren/tsd4とMicrosoft謹製のdtslint5が挙げられますが、この記事ではtsdを使ったテストを書いてみます6。
テストランナーとしてJestを使い、tsdからアサーションをインポートして利用します。
npm install
npm install -D tsd jest @types/jest ts-jest
package.json
package.json
のトップレベルにjest
というキーを追加して設定を書きます(独立ファイルにもできる)。
{
...
"scripts": {
"build": "tsc",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "jest"
},
...
+ "jest": {
+ "preset": "ts-jest",
+ "verbose": true,
+ "moduleFileExtensions": [
+ "ts",
+ "js"
+ ],
+ "testMatch": [
+ "**/types/*.test-d.ts"
+ ]
+ }
+ }
テストファイル
テストファイルtypes/is-positive-integer.test-d.ts
を作るのですが、作ると同時に(中身を書く前に)tsconfig.json
のfiles
に同ファイル名を追加しておくと、VS Codeで補完が聞くようになり便利です。
files
に*.test-d.ts
を含めるとビルド時にトランスパイルされてdist/
配下に*.test-d.js
として出力されることになります。ここでもpackage.json
のscript.postbuild
で削除するなどが選択肢になるでしょう。
{
"compilerOptions": {
...
},
"files": [
"index.ts",
"types/is-positive-integer.d.ts",
+ "types/is-positive-integer.test-d.ts",
],
}
import { expectType } from 'tsd';
import isPositiveInteger from 'is-positive-integer';
describe('isPositiveInteger', () => {
it('returns boolean', () => {
expectType<boolean>(isPositiveInteger(1));
expectType<boolean>(isPositiveInteger(0));
})
});
このテストは型のテストをしているというよりも関数が返す値の型をチェックしている状態ですが、ひとまず型の一致をテストできていることを確認してください。
テストを実行するにはnum test
コマンドまたはnpm run test
コマンドを実行します。
もう少しテストらしいテスト
前段のテストは「型のテスト」とはいえない感じだったので、TypeScriptに組み込まれているUtility TypesからRequired<Type>
のテストを書いてみます7。このユーティリティ型は、OptionalなプロパティをOptionalではなく(=必須に)します。例えば次のような型があるとして
interface Foo {
a?: number;
b?: string;
}
この型をRequired<Foo>
すると、キーa
とb
に付いている?
が外れて両方が必須となる型が得られます。
import {
expectType,
expectAssignable,
expectNotAssignable
} from 'tsd';
describe('Required', () => {
it('makes props required', () => {
interface Fixture {
a?: number;
b?: string;
}
expectType<Required<Fixture>>({
a: 1,
b: 'one',
})
expectAssignable<Required<Fixture>>({
a: 1,
b: 'one',
});
expectNotAssignable<Required<Fixture>>({
a: 1,
});
expectNotAssignable<Required<Fixture>>({
b: 'one',
});
});
});
参照
- typeRootsの誤解 -- TypeScriptで、npmからインストールしたパッケージに型定義ファイル (*.d.ts) が存在しない場合の正しい対処方法 - Qiita
- TypeScript型定義ファイルのコツと生成ツール dtsmake - Qiita
- TypeScript: Documentation - .jsファイルから.d.tsファイルを生成する
- TypeScript: TSConfig リファレンス - すべてのTSConfigのオプションのドキュメント
- tsdでTypeScriptの型定義とテストで戦う | Kumasan
-
npx dts-gen -m is-positive-integer.d.ts
で生成してもいい(Definitely Typedなどでは推奨されている)のですが、is-positive-integer
はシンプルな構成で、かつdts-gen
の結果があまりうまくいかなかったので、ゼロから書きました。 ↩ -
グローバルライブラリについては https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-d-ts.html https://www.typescriptlang.org/docs/handbook/declaration-files/library-structures.html#consuming-dependencies などを参照。 ↩
-
tsdという名前はもともと、型定義ファイルパッケージ管理を行うDefinitelyTyped/tsdで使われていましたが、2016年にDefinitely Typedで非推奨とされました。代わりのパッケージ管理ツールとしてtypings/typingsが出たのが非推奨の理由でしたが、2022年現在ではこのtypingsもdeprecatedとなっているようです。型テストライブラリとしてのtsdは2018年から開発が始まり、npm trendsによると2021年4月を境にdtslintよりも人気になっています。 ↩
-
リポジトリとしてはmicrosoft/DefinitelyTyped-toolsの一部となっています。 ↩
-
dtslintは、2019年にdeprecatedとなったTSLintに依存しており、やや不安を感じます。 ↩
-
Required<Type>
の実装はソースや @uhyo さんによるTypeScriptの型入門のMapped Typesの節などを参照。 ↩