TypeScriptの型定義ファイル（d.ts）を読み書き

## はじめに

この記事はこのような方を対象にしてます。
- TSは普段使っているけどd.tsファイルの読み方がいまいちわからない。
- d.tsファイルはなんとなく読み書きできるけど記述するときにつまづく時がある。

## d.tsファイル、意外とややこしい

「意外とややこしい」というのは個人の所感にはなりますが、そう思っている人もきっといるはず。。。

d.tsファイルは端的にいうと、明示的に型付されていないJSのモジュールに対して、型をつけてTSでハッピーになるためのファイルです。

実際にd.tsファイルに触れる機会といえば、TSのプロジェクトでIDEやエディタから定義ジャンプして型を確認したり、ライブラリ作者やコミッターであれば、d.tsファイルを生成したり、記述したりすることがあるでしょう。

しかし、実際にd.tsファイルを記述する機会は人によっては少なくい場合も多く、自分自身d.ts特有のキーワードや記述に困惑することもよくあったので、備忘録も兼ねてこの記事にまとめてみた。

## d.tsファイルを読み書きする前に

まず、d.tsファイルを読み書きするのに、前提として重要なポイントがあります。

ファイルが**モジュール**なのか**スクリプト**なのか

この前提があることで、d.tsのルールがより理解できるでしょう。

### モジュールとは

意味合いとしてはJSのモジュールシステムとほぼ同じです。TypeScriptのコンパイラは`export`or`import`キーワードを見つけると、そのファイルはモジュールファイルであると認識します。モジュールのコードはどこからでもアクセスできるものではなく、明示的に`export`し、使用するファイルから`import`してやる必要があります。


### スクリプトとは

例えば、jQueryを使用したことがる人はこのような記述をしたことがあるのではないでしょうか。

```html:index.html
<script src="/path/to/jquery.min.js"></script>
<script src="/path/to/main.js"></script>
```

```js:main.js
$(function () {
	const $element = $('.js-target-element');
	// do something...
});
```

このコードスニペットでは、`$`のjQueryオブジェクトが`import`されることなく、参照されているが、これはつまり、`jquery.min.js`を実行したことで、`window`オブジェクトからjQueryオブジェクトが生えた状態と言えるわけです。ちなみに、TSの世界ではUMDモジュールという名前がよく出てくるが、`window`オブジェクトにモジュールを生やすのと、`exports`オブジェクトにモジュールを代入するCommonJSを1ファイルで兼用することができる仕組みのことです。UMDモジュールではCommonJS環境であれば`exports`オブジェクトに、ブラウザ環境では`window`オブジェクトにモジュールを生やします。

この**モジュール**と**スクリプト**という２つの世界観に分けてd.tsについて書いていく。

## モジュールのd.ts

### 基本形

前述した通り、モジュールには`export`、`import`キーワードが必要す。例えば、以下のようなJSモジュールを想定する。

```js:mod.js
export function sum(num1, num2) {
	return num1 + num2	
}

export function sayHello(name) {
	console.log(`Hello, ${name}!`)
}
```

d.tsは以下

```ts:mod.d.ts
export function sum(num1: number, num2: number): number;

export function sayHello(name: string): void;
```

ここまでは想像に難くないはず。

### declare global

ライブラリの型定義でよく見かける`declare global`から始まる記述について。

その名の通り、グローバルに型定義をするが、モジュール内でのみ使用可能。スクリプトd.tsでは`declare global`を記述できません。例えば、下記のコードを考えてみましょう。

```js:mod.js
window.hoge = hoge;

export function hoge() {
	//
}
```

```js:main.ts
import './mod';

window.hoge()
```

例に挙げた`mod.js`の型定義ファイル`mod.d.ts`を記述するとしたら下記。

```ts:mod.d.ts
declare global {
	interface Window {
		hoge: typeof hoge
	}
}

export function hoge(): void
```

### export as namespace

`export as namespace`。こちらもグローバルスコープからアクセスできるものを定義するキーワードだが、モジュールd.tsでのみ使用可能。これによってグローバルスコープから定義にアクセスできる。どれにアクセスできるかは、参照しているファイルがモジュールかスクリプトかによって異なってくる。

型定義
```ts:mod.d.ts
export function hoge(args: Hoge.Args): void

interface Args {
	foo: string;
	bar: boolean;
}

export as namespace Hoge
```

モジュールからは型定義にのみアクセスできる。
```ts:main.ts
import { hoge } from './mod';

const hogeArgs: Hoge.Args = {
	foo: '...',
	bar: false,
};

hoge(hogeArgs);
```

スクリプトからは型定義以外にもアクセスできる。
```ts:script.ts
const hogeArgs: Hoge.Args = {
	foo: '...',
	bar: false,
};

Hoge.hoge(hogeArgs);
```

## スクリプトのd.ts

スクリプトのd.tsの記述は比較的シンプルです。`export`と`import`を含まないファイルのこと。

サードパーティースクリプトやアナリティクスのコードに対して定義することが多い。TypeScriptコンパイラにグローバルからアクセスできるものとして宣言する。

```ts:script.d.ts
const dataLayer: unkwon[];

function doSomething(): void;

interface Buzz {
	a: string;
	b: string;
}
```

```ts:main.ts
function handleClick() {
	dataLayer.push({ event: '...' });
	doSomething();
}

const buzz: Buzz = { a: '...', b: '...' };
```

尚、グローバルスコープの`interface`を拡張する場合、モジュールと違って`declare global`の記述は不要。

```ts:script.d.ts
interface Window {
	hoge(): void
}
```

```ts:main.ts
window.hoge();
```

## 既存のモジュールの型を拡張

既存のモジュールの`interface`に新しいプロパティを追加したいとなったとする。（[Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)という）例えば元のモジュールの型定義がこちらだった場合、

```ts:original.d.ts
export interface Hoge {
	a: string;
	b: string;
}

export interface Foo {
	a: number;
	b: number;
}
```

これはモジュールの`import`先のファイルで意図通りにモジュールの型定義を上書きできない。
```ts:extend.d.ts
declare module 'original' {
	interface Hoge {
		c: string
	}
}
```

この例ではスクリプトと認識されるため、[Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)ではなく、[Ambient Module](https://www.typescriptlang.org/docs/handbook/modules/reference.html#ambient-modules)になる。

```ts:extend.d.ts
declare module 'original' {
	interface Hoge {
		c: string
	}
}

export {} // 追加
```

`export`キーワードを追加して、TypeScriptコンパイラにモジュールと認識させることが必要。

## 最後に

別のプロジェクトではエラーにならなかったのに、今のプロジェクトで同じ型定義をしようとしたらなぜかエラーになったというときなどに、この記事がヒントになれば幸いです。

もっと詳しく知りたい場合は[公式のリファレンス](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html)を参照してください。