TypeScript で複雑な型定義をするにあたり「テストを書きながら効率的に行いたい!」と思ったことはありませんか?TypeScript では Compilerレイヤーの API が Node.js に向け公開されており、環境コンテキストを加味した推論まで取得することが可能です。
本稿では、CompilerAPI をテストに活用するアプローチを紹介します。
tl;dr
サンプルリポジトリを用意しました。
https://github.com/takefumi-yoshii/ts-type-inference-test
const w0 = 0;
const n1 = 1 as const;
const n2 = 2 as 2;
const _w0 = { val: w0 }["val"];
const _n1 = { val: n1 }["val"];
const _n2 = { val: n2 }["val"];
こちらのファイルと対になるテストは、次の様に書かれています。
const srcFileName = `__tests__/widening.ts`;
const infer = createDeclarationInferencer(srcFileName);
// Widening Literal Type の挙動をテストで確認する
test("Widening Literal Type looks like as literal.", () => {
expect(infer("w0")).toBe("0");
expect(infer("n1")).toBe("1");
expect(infer("n2")).toBe("2");
});
test("Widening Literal Type go unnoticed until used.", () => {
expect(infer("_w0")).toBe("number");
expect(infer("_n1")).toBe("1");
expect(infer("_n2")).toBe("2");
});
変数「w0」にマウスオーバーし『ヨシ!0が推論されてるね』と目視で確認していた作業そのものが、テストコードに落とし込まれていることが伺えます。_w0
の推論がnumber
になってしまうところが、Widening Literal Type の特徴ですね。
テスト対象は推論結果
filter処理を施した配列を、変数に代入。この変数を調べてみます。
const arr = [1, 2, 3, undefined, null];
const res1 = arr.filter(removeUndefined1);
const res2 = arr.filter(removeUndefined2);
const res3 = arr.filter(removeUndefined3);
配列のfilterには、User Defined Type Guard を利用することが定石ですね。
function removeUndefined1<T>(args: T) {
return args !== undefined;
}
function removeUndefined2<T>(args: T): args is Exclude<T, undefined> {
return args !== undefined;
}
function removeUndefined3<T>(args: T): args is NonNullable<T> {
return args !== undefined;
}
以下のテストは全てパスします。型情報を文字列として解釈していることがわかります。
test("Changes Inference by User Defined Type Guard", () => {
expect(infer("res1")).toBe("(number | null | undefined)[]");
expect(infer("res2")).toBe("(number | null)[]");
expect(infer("res3")).toBe("number[]");
});
infer関数
infer関数は、特定ファイルで宣言されている、変数の型推論を調べることができます。
ASTをトラバースし、変数宣言に適用されている型を、文字として取得しています。
export function getInferredTypeStringByDeclarationName(
checker: ts.TypeChecker,
source: ts.SourceFile,
declarationName: string
) {
let res: string | undefined;
function visit(node: ts.Node) {
switch (node.kind) {
// 変数宣言 Node であり
case ts.SyntaxKind.VariableDeclaration:
// 変数名が 期待値である場合
const name = node.getChildAt(0).getText();
if (name === declarationName) {
// ts.TypeChecker で推論適用されている型を得る
const type = checker.getTypeAtLocation(node);
// 型情報を文字列化する
res = checker.typeToString(type);
}
break;
}
if (res) return;
ts.forEachChild(node, visit);
}
visit(source);
return res;
}
特筆すべきは、今回もts.TypeChecker
です。特定の Node まで絞り込み、その Node を対象に TypeChecker の API を適用することで、様々な情報を引き出すことが可能です。
まとめ
CompilerAPI を利用した、複雑な型定義のテスト手法はこの限りではありませんので、要件に応じてトラバーサーを書くと良いと思います。VS Code で知り得る情報ならば、だいたい Node.js Application に落とし込めると見込んで良いでしょう。