LoginSignup
47
45

More than 3 years have passed since last update.

TypeScript の型推論をテストする

Last updated at Posted at 2020-06-05

TypeScript で複雑な型定義をするにあたり「テストを書きながら効率的に行いたい!」と思ったことはありませんか?TypeScript では Compilerレイヤーの API が Node.js に向け公開されており、環境コンテキストを加味した推論まで取得することが可能です。

本稿では、CompilerAPI をテストに活用するアプローチを紹介します。

tl;dr

サンプルリポジトリを用意しました。
https://github.com/takefumi-yoshii/ts-type-inference-test

__tests__/widening.ts
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"];

こちらのファイルと対になるテストは、次の様に書かれています。

__tests__/widening.test.ts
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処理を施した配列を、変数に代入。この変数を調べてみます。

__tests__/userDefinedTypeGuard.ts
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 を利用することが定石ですね。

__tests__/userDefinedTypeGuard.ts
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;
}

以下のテストは全てパスします。型情報を文字列として解釈していることがわかります。

__tests__/userDefinedTypeGuard.test.ts
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をトラバースし、変数宣言に適用されている型を、文字として取得しています。

__tests__/__utils__/traverser.ts
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 に落とし込めると見込んで良いでしょう。

47
45
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
45