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
でチェックする(正しくない場合に型エラーを発生させる)
…という流れ。
-
関数の戻り値の型がうっかり変わってしまっていないかをチェックするなど、特にライブラリを書くときに便利な機能だと感じました。