[Deno] TypeScriptの "型定義" をテストする方法（testing/typesモジュール）

Denoの標準ライブラリにはTypeScriptの「型定義」をテストするための`testing/types`モジュールがあります。この記事はそれの紹介です。

https://deno.land/std@0.180.0/testing/types.ts

## 型定義のテストとは

[Conditional Type](https://zenn.dev/oreo2990/articles/1040312d7af066)を使用して型の条件分岐を行っている時や、引数に指定する値によって戻り値の型が変わる関数の場合、「正しい型が返ってきているかどうか」をテストしたい場合があります。

例として以下のような関数を考えます。

```ts
function helloOrWorld<T extends string>(arg: T): T extends true ? "hello" : T extends false ? "world" : "hello" | "world" {
  // 実装は省略
}

helloOrWorld(true); // この戻り値の型が正しいかどうかチェックしたい
```

従来は`helloOrWorld(true)`のところにマウスを当て、手動で正しい型が返ってきているかを確認していました。
これを**テストコードとして書くことができ、CIでチェックできるようになる**のが、今回紹介する[testing/typesモジュール](https://deno.land/std@0.180.0/testing/types.ts)です！

:::note
Deno以外の環境における型定義のテストについては、以下の記事も参考になります。

- [TypeScript の型推論をテストする](https://qiita.com/Takepepe/items/3e65b110cff819e83b4f)
- [tsdでTypeScriptの型定義とテストで戦う](https://kumaaaaa.com/tsd-type-unit-testing/)
- [型レベルユニットテストのススメ](https://zenn.dev/arark/articles/356a13c38a612b)
:::

## testing/typesモジュールの使い方

testing/typesモジュールの使い方を解説します。

testing/typesモジュールには、型定義のテストに使える以下のユーティリティが含まれています。

:::note warn
これらのユーティリティは型チェック時のみに意味があります。**実行時には何もチェックしてくれません**。
:::

重要度|関数名|使用例|説明
:--:|--|--|--
<span></span>|`Assert<T, Expected>`|`Assert<Has<...>, true>`|型`T`が型`Expected`(true又はfalse)と一致しなければ型エラー
★★★|`AssertTrue<T>`|`AssertTrue<IsExact<...>>`|型`T`がtrueと一致しなければ型エラー
<span></span>|`AssertFalse<T>`|`AssertFalse<Has<...>>`|型`T`がfalseと一致しなければ型エラー
|||
★|`Has<T, U>`|`Has<100, typeof foo>`|型`T`が型`U`に含まれていればtrue、そうでなければfalse
<span></span>|`NotHas<T, U>`|`NotHas<"hello", typeof foo>`|型`T`が型`U`に含まれて**いなければ**true、そうでなければfalse
★★★|`IsExact<T, U>`|`IsExact<string\|number, typeof foo>`|型`T`と型`U`が正確に一致すればtrue、そうでなければfalse
<span></span>|`IsAny<T>`|`IsAny<typeof foo>`|型`T`がany型ならtrue、そうでなければfalse
<span></span>|`IsNever<T>`|`IsNever<typeof foo>`|型`T`がnever型ならtrue、そうでなければfalse
<span></span>|`IsNullable<T>`|`IsNullable<typeof foo>`|型`T`がnullかundefinedならtrue、そうでなければfalse
<span></span>|`IsUnknown<T>`|`IsUnknown<typeof foo>`|型`T`がunknown型ならtrue、そうでなければfalse

上記のユーティリティを使用してテストを書くことで、正しい型になっていれば型チェックが通り、**間違った型であれば型チェックが失敗する**ようなコードを書くことができます。

テストを書くには、

1. まず下7つの**チェック用ユーティリティ（`Has`や`IsExact`など）を使用して`true`や`false`を返し**、
1. それを上3つのアサーション用ユーティリティ（`AssertTrue`や`AssertFalse`など）にかけて**期待した値でなければ型エラーを起こす**

というやり方で書きます。

具体的には、以下のようなコードになります。

```ts
import type {
  AssertTrue,
  IsExact,
} from "https://deno.land/std@0.180.0/testing/types.ts";

Deno.test({
  name: "typing test",
  fn() {
    const val = mySuperFunction();
    type _ = AssertTrue<IsExact<typeof val, string>>; // この行で型チェックしている
    // typeof val が string型と正確に一致するかどうか（一致しなければ型エラーが起こる）
  },
});
```

上記の例では`mySuperFunction`という関数の返り値の型が、`string`型と正確に一致するかどうかを調べています。
まず`typeof val`で変数の型を取り、`IsExact`で目的の型と一致するか調べます。`IsExact`はtrueまたはfalseを返すため、それを`AssertTrue`に渡すことによって、trueでない場合に型エラーを発生させています。

`IsExact`の代わりに`Has`や`IsNullable`を使いたい時も、同様の手法で実現できます。

型エラーが起こる場合、エディタ上で赤波線が引かれ、エラー扱いになるはずです。
これを**CIなどでコマンドラインからチェックしたい時には、`deno test`コマンドを使います**。

```shell
> deno test
```

なお、`deno run`コマンドでは`--check`オプションを渡さないと型チェックが走らないため、型エラーを検出できません。CI等で型チェックをかけたい場合は、`deno test`コマンドを使いましょう。

## まとめ

- 型定義のテストにはtesting/typesモジュールを使う
  - https://deno.land/std@0.180.0/testing/types.ts
- 基本パターンは、
    1. `typeof`で変数の型を取る
    1. `IsExtract`や`Has`で型が正しいかどうか調べる
    1. その結果がtrueかfalseかを、`AssertTrue`や`AssertFalse`でチェックする（正しくない場合に型エラーを発生させる）
 …という流れ。

関数の戻り値の型がうっかり変わってしまっていないかをチェックするなど、特にライブラリを書くときに便利な機能だと感じました。