Denoの標準ライブラリにはTypeScriptの「型定義」をテストするためのtesting/typesモジュールがあります。この記事はそれの紹介です。
型定義のテストとは
Conditional Typeを使用して型の条件分岐を行っている時や、引数に指定する値によって戻り値の型が変わる関数の場合、「正しい型が返ってきているかどうか」をテストしたい場合があります。
例として以下のような関数を考えます。
function helloOrWorld<T extends string>(arg: T): T extends true ? "hello" : T extends false ? "world" : "hello" | "world" {
// 実装は省略
}
helloOrWorld(true); // この戻り値の型が正しいかどうかチェックしたい
従来はhelloOrWorld(true)のところにマウスを当て、手動で正しい型が返ってきているかを確認していました。
これをテストコードとして書くことができ、CIでチェックできるようになるのが、今回紹介するtesting/typesモジュールです!
Deno以外の環境における型定義のテストについては、以下の記事も参考になります。
testing/typesモジュールの使い方
testing/typesモジュールの使い方を解説します。
testing/typesモジュールには、型定義のテストに使える以下のユーティリティが含まれています。
これらのユーティリティは型チェック時のみに意味があります。実行時には何もチェックしてくれません。
| 重要度 | 関数名 | 使用例 | 説明 |
|---|---|---|---|
Assert<T, Expected> |
Assert<Has<...>, true> |
型Tが型Expected(true又はfalse)と一致しなければ型エラー |
|
| ★★★ | AssertTrue<T> |
AssertTrue<IsExact<...>> |
型Tがtrueと一致しなければ型エラー |
AssertFalse<T> |
AssertFalse<Has<...>> |
型Tがfalseと一致しなければ型エラー |
|
| ★ | Has<T, U> |
Has<100, typeof foo> |
型Tが型Uに含まれていればtrue、そうでなければfalse |
NotHas<T, U> |
NotHas<"hello", typeof foo> |
型Tが型Uに含まれていなければtrue、そうでなければfalse |
|
| ★★★ | IsExact<T, U> |
IsExact<string|number, typeof foo> |
型Tと型Uが正確に一致すればtrue、そうでなければfalse |
IsAny<T> |
IsAny<typeof foo> |
型Tがany型ならtrue、そうでなければfalse |
|
IsNever<T> |
IsNever<typeof foo> |
型Tがnever型ならtrue、そうでなければfalse |
|
IsNullable<T> |
IsNullable<typeof foo> |
型Tがnullかundefinedならtrue、そうでなければfalse |
|
IsUnknown<T> |
IsUnknown<typeof foo> |
型Tがunknown型ならtrue、そうでなければfalse |
上記のユーティリティを使用してテストを書くことで、正しい型になっていれば型チェックが通り、間違った型であれば型チェックが失敗するようなコードを書くことができます。
テストを書くには、
- まず下7つのチェック用ユーティリティ(
HasやIsExactなど)を使用してtrueやfalseを返し、 - それを上3つのアサーション用ユーティリティ(
AssertTrueやAssertFalseなど)にかけて期待した値でなければ型エラーを起こす
というやり方で書きます。
具体的には、以下のようなコードになります。
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コマンドを使います。
> deno test
なお、deno runコマンドでは--checkオプションを渡さないと型チェックが走らないため、型エラーを検出できません。CI等で型チェックをかけたい場合は、deno testコマンドを使いましょう。
まとめ
- 型定義のテストにはtesting/typesモジュールを使う
- 基本パターンは、
-
typeofで変数の型を取る -
IsExtractやHasで型が正しいかどうか調べる - その結果がtrueかfalseかを、
AssertTrueやAssertFalseでチェックする(正しくない場合に型エラーを発生させる)
…という流れ。
-
関数の戻り値の型がうっかり変わってしまっていないかをチェックするなど、特にライブラリを書くときに便利な機能だと感じました。