LoginSignup
3
0

More than 1 year has passed since last update.

自分のTypeScriptプロジェクト専用に他人のNPMパッケージの型定義(アンビエント宣言)を書く

Last updated at Posted at 2022-07-11

この記事は以下のような需要に応えようとするものです。

  • 他人の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

シンプルな設定にとどめます。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "moduleResolution": "node",
    "declaration": true,
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
  },
  "files": [
    "index.ts",
  ],
}

トランスパイルされた*.js*.d.tsdist/ディレクトリの下に出力される設定です。

ファイル数が少ないのでTypeScriptソースをfilesキーで指定していますが、globを使えるincludeキー"include": ["src/**/*.ts"]のようにすることもできます。

package.json

package.jsonmaintypesscripts.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ソースです。

index.ts
import isPositiveInteger from 'is-positive-integer'

export function posiOrNega(n: number): string {
  return isPositiveInteger(n) ? 'Positive!' : 'Not Positive!';
}

型情報を書く

やっと型情報を書く段階にきました。といっても関数2つなので大したことはありません。型情報ファイルの内容よりも、プロジェクトの設定にそのファイルをどう反映させるかや、パッケージ内でどう扱うかのほうがポイントです。

is-positive-integer.d.ts

型情報ファイルを作ります2typesというサブディレクトリを作り、types/is-positive-integer.d.tsとします。

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に登録し、コンパイラに型情報の位置を伝えます。

tsconfig.json
 {
   "compilerOptions": {
     ...
   },
   "files": [
     "index.ts",
+    "types/is-positive-integer.d.ts",
   ],
 }

なお、compilerOptions.typeRootsimportするモジュールの型情報を示すためのオプションではなく、今回の場合、使ってもコンパイラに型情報は伝わりません(いかにも使えそうなオブション名ですが)。

typeRootsはグローバルライブラリ3を参照する/// <reference types="something" />を使ったときに、参照モジュールを探索する場所を指定する設定です。

compilerOptions.typeRootsについては @tetradice さんが以下の記事で詳しくまとめてくださっています。

依存パッケージの型情報ファイルを公開するかどうか

tsconfig.jsonfilestypes/is-positive-integer.d.tsを追加したことにより、型情報ファイルがビルド時にcompilerOptions.outDir(今回はdist/)へコピーされることになります。

もしnpm publishする場合、他パッケージから参照されるdistディレクトリに別パッケージの型情報を残してしまうのは行儀が良くなさそうです。今回はtscだけでトランスパイルしているので、package.jsonscripts.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というキーを追加して設定を書きます(独立ファイルにもできる)。

package.json
  {
    ...
    "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.jsonfilesに同ファイル名を追加しておくと、VS Codeで補完が聞くようになり便利です。

files*.test-d.tsを含めるとビルド時にトランスパイルされてdist/配下に*.test-d.jsとして出力されることになります。ここでもpackage.jsonscript.postbuildで削除するなどが選択肢になるでしょう。

tsconfig.json
  {
    "compilerOptions": {
      ...
    },
    "files": [
      "index.ts",
      "types/is-positive-integer.d.ts",
+     "types/is-positive-integer.test-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>すると、キーabに付いている?が外れて両方が必須となる型が得られます。

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',
    });
  });
});

参照

  1. NPMとleft-pad : 私たちはプログラミングのやり方を忘れてしまったのか? | POSTD で知りました。

  2. npx dts-gen -m is-positive-integer.d.tsで生成してもいい(Definitely Typedなどでは推奨されている)のですが、is-positive-integerはシンプルな構成で、かつdts-genの結果があまりうまくいかなかったので、ゼロから書きました。

  3. グローバルライブラリについては 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 などを参照。

  4. tsdという名前はもともと、型定義ファイルパッケージ管理を行うDefinitelyTyped/tsdで使われていましたが、2016年にDefinitely Typedで非推奨とされました。代わりのパッケージ管理ツールとしてtypings/typingsが出たのが非推奨の理由でしたが、2022年現在ではこのtypingsもdeprecatedとなっているようです。型テストライブラリとしてのtsdは2018年から開発が始まり、npm trendsによると2021年4月を境にdtslintよりも人気になっています。

  5. リポジトリとしてはmicrosoft/DefinitelyTyped-toolsの一部となっています。

  6. dtslintは、2019年にdeprecatedとなったTSLintに依存しており、やや不安を感じます。

  7. Required<Type>の実装はソース@uhyo さんによるTypeScriptの型入門のMapped Typesの節などを参照。

3
0
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
3
0