TypeScript の型定義ファイル(d.ts)周りの仕様を確認してみた

2024.01.31

こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。

今回は、TypeScript の型定義ファイル(d.ts)周りの仕様を検証をしながら確認する機会があったのでご紹介します。

検証

Node.js & TypeScript のプロジェクト作成

検証環境として Node.js & TypeScript のプロジェクトを新規作成します。作成手順は下記を参考にします。

環境作成用のコマンドを実行します。

# package.json を作成
npm init -y

# TypeScript をインストール
npm install typescript --save-dev

# TSConfig を作成
npx tsc --init --rootDir src --outDir lib --esModuleInterop --resolveJsonModule --lib es6,dom --module commonjs

@types/node の導入

ここで src/sample.ts ファイルを作成します。

mkdir src
touch src/sample.ts

ファイル内容は下記の通りです。Node.js の os モジュールをインポートして使用しています。

src/sample.ts

import os from "os"

console.log("Platform: " + os.platform());

ここで tsc(TypeScript コンパイラ)を実行します。--noEmit オプションを付けることでコンパイル結果の出力を抑制しています。

$ tsc --noEmit
src/sample.ts:1:16 - error TS2307: Cannot find module 'os' or its corresponding type declarations.

1 import os from "os"
                 ~~~~


Found 1 error in src/sample.ts:1

実行結果は、os に対応する型宣言(corresponding type declarations)が見つからないというエラーになりました。

ここで Node.js のプログラムに必要な型定義ファイルである @types/node をインストールします。

npm install @types/node --save-dev

これにより node_modules/@types/node ディレクトリ配下に型定義ファイルが作成されます。以下はその一部ですが、Node.js で使用できるモジュールの型宣言が含まれています。

$ tree node_modules/@types -L 2
node_modules/@types
└── node
    ├── LICENSE
    ├── README.md
    ├── assert
    ├── assert.d.ts
    ├── async_hooks.d.ts
    ├── buffer.d.ts
    ├── child_process.d.ts
    ├── cluster.d.ts
    ├── console.d.ts
    ├── constants.d.ts
    ├── crypto.d.ts
    ├── dgram.d.ts
    ├── diagnostics_channel.d.ts
    ├── dns
    ├── dns.d.ts
    ├── dom-events.d.ts
    ├── domain.d.ts
    ├── events.d.ts
    ├── fs
    ├── fs.d.ts
    ├── globals.d.ts
    ├── globals.global.d.ts
    ├── http.d.ts
    ├── http2.d.ts
    ├── https.d.ts
    ├── index.d.ts
    ├── inspector.d.ts
    ├── module.d.ts
    ├── net.d.ts
    ├── os.d.ts
    ├── package.json
    ├── path.d.ts
    ├── perf_hooks.d.ts
    ├── process.d.ts
    ├── punycode.d.ts
    ├── querystring.d.ts
    ├── readline
    ├── readline.d.ts
    ├── repl.d.ts
    ├── stream
    ├── stream.d.ts
    ├── string_decoder.d.ts
    ├── test.d.ts
    ├── timers
    ├── timers.d.ts
    ├── tls.d.ts
    ├── trace_events.d.ts
    ├── ts4.8
    ├── tty.d.ts
    ├── url.d.ts
    ├── util.d.ts
    ├── v8.d.ts
    ├── vm.d.ts
    ├── wasi.d.ts
    ├── worker_threads.d.ts
    └── zlib.d.ts

すると tsc が正常にパスするようになりました。

tsc --noEmit

ここで、npm の node パッケージの配信ページを確認すると、パッケージ名の右横に「DT」アイコンが表示されています。これは型定義ファイルがこのパッケージ自身には含まれていないが、DefinitelyTyped に登録されていることを示しています。

上記の「DT」アイコンのリンクから、下記のように型定義ファイルの配信ページに遷移できます。node の場合はこの @types/node を別途インストールする必要がある、ということが分かります。

また DefinitelyTyped は GitHub リポジトリで管理されています。

下記のように多くのパッケージの型定義ファイルが管理されています。

コンパイル実行

次に TypeScript のコンパイル実行について確認します。

下記の ts ファイルを作成します。

src/sample.ts

interface Person {
  firstName: string;
  lastName: string;
}
 
function greeter(person: Person): string {
  return "Hello, " + person.firstName + " " + person.lastName;
}

tsc(TypeScript コンパイル)コマンドを実行します。

tsc

すると lib ディレクトリに Node.js の実行可能ファイル (js) が作成されます。

lib/sample.js

"use strict";
function greeter(person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

また同じコマンドを -d オプション付きで実行します。

tsc -d

すると、lib ディレクトリに型定義ファイル(d.ts)が作成されます。

lib/sample.d.ts

interface Person {
    firstName: string;
    lastName: string;
}
declare function greeter(person: Person): string;

src/sample.ts ファイルでは、TypeScript による上記の型宣言が含まれていることが分かります。

型定義ファイルの自作

さてここまでは型定義ファイルを npm からインストールまたは tsc による自動生成によって作成してきましたが、最後に自作をしてみたいと思います。

まずは src/sample.ts ファイルを下記のように修正し、Person の型宣言を削除します。

src/sample.ts

// interface Person {
//   firstName: string;
//   lastName: string;
// }
 
function greeter(person: Person): string {
  return "Hello, " + person.firstName + " " + person.lastName;
}

tsc を実行すると、Person に対応する型定義ファイルが見つからないというエラーになります。

$ tsc --noEmit    
src/sample.ts:6:26 - error TS2304: Cannot find name 'Person'.

6 function greeter(person: Person): string {
                           ~~~~~~


Found 1 error in src/sample.ts:6

そこで、先程作成した d.ts ファイルをコピーして src/types/sample.d.ts ファイルを作成します。

mkdir src/types
cp lib/sample.d.ts src/types/sample.d.ts

再度 tsc を実行すると今度はパスしました。types ディレクトリ配下の型定義ファイルが参照されていることが分かります。

tsc --noEmit

さらに src/sample.ts ファイルを下記のように修正し、関数宣言 greeter を削除した上で greeter 関数を呼び出すようにします。

src/sample.ts

// interface Person {
//   firstName: string;
//   lastName: string;
// }
 
// function greeter(person: Person): string {
//   return "Hello, " + person.firstName + " " + person.lastName;
// }

greeter({firstName: 'Jane', lastName: 'User'})

tsc を実行するとこちらもパスしました。greeter 関数の実装は削除されていますが、types ディレクトリ配下に型宣言があることでコンパイルが通りました。

tsc --noEmit

同様の仕様を利用することにより Jest や Vitest などのテストフレームワークで見られるようなグローバル関数(import 不要の関数)は実現されています。

おわりに

TypeScript の型定義ファイル(d.ts)周りの仕様を検証をしながら確認する機会があったのでご紹介しました。

今まで曖昧にしか理解できていなかった TypeScript の型宣言の仕組みが少し理解できました。やはり公式(もしくは信頼の置ける)ドキュメントを読みながら手を動かすことが理解への一番の近道であることを改めて痛感しました。

参考

以上