0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScript の型関数を簡単にテストする

Last updated at Posted at 2021-06-06

0. 注意

tsd のようなものをお求めの方は申し訳ありませんが、この記事では単に tsc --noEmit ./test/ 相当のことをします。

1. 複雑な型定義に関する課題

TypeScript の制約をとても強くしていくと、かなり複雑な型関数が現れることがあります。

(この記事では)理解する必要はありませんが、例えばこんなのとか1

type ToTuple<T extends string, Result extends string[] = []> =
    T extends `${infer U}${infer V}`
    ? ToTuple<V, [...Result, U]>
    : Result;

あるいはこんなのとか2

type DeepReadonly<T> =
    T extends any[] ? DeepReadonlyArray<T[number]> :
    T extends object ? DeepReadonlyObject<T> :
    T;

interface DeepReadonlyArray<T>
    extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
    readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};

type NonFunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? never : K
}[keyof T];

このような複雑な型関数の挙動を一目で理解するのは TypeScript 素人にとって、とても難しいことです。

一番目の型関数は再帰制限を知らない人にもっと簡単な型に書き直されてしまうかもしれず、コメントを念入りに書く必要があるでしょう。

「型の StoryBook3」がほしいところです。

2. 型の StoryBook を作る――するとテストになる

2-1. 型の StoryBook を作る

先の DeepReadonly の挙動が確認できるように、いろいろなパターンを試してみましょう。

エラーをわざと出して、@ts-expect-error コメントで制御します4

typebook/deepReadonly.ts
import type { DeepReadonly } from 'utility/types/deepReadonly';

type DeepArr = [number[], string[]];

// ふつうの Readonly<T>
type ReadonlyArr = Readonly<DeepArr>;
const arr: ReadonlyArr = [[], []];
// @ts-expect-error
arr[0] = [];
arr[1][0] = 'str';
// @ts-expect-error
arr.push(['str']);
arr[0].push(2);
arr[0][1] = 3;

// DeepReadonly<T>
type DeepReadonlyArr = DeepReadonly<DeepArr>;
const arr1: DeepReadonlyArr = [[], []];
// @ts-expect-error
arr1[0] = [];
// @ts-expect-error
arr1[1][0] = 'str';
// @ts-expect-error
arr1.push(['str']);
// @ts-expect-error
arr1[0].push(2);
// @ts-expect-error
arr1[0][1] = 3;

挙動が目に見える形になり、なんとなくどういう型関数なのかわかってきたのではないでしょうか?

今回は例に過ぎないのでこのくらいにしておきますが、さらに Object についても追加すると、より一層挙動がわかりやすくなるでしょう。

2-2. 型の StoryBook を自動テストにする

さて、これを自動テストにしていきましょう。

といっても、すでに mochajest 等を用いて適切に .ts ファイルの自動テストが適切に設定されていれば、とても簡単です。

そうでない場合もテストライブラリなしで書けます。

2-2-1. すでに ts-jest 等を利用したテスト環境がある場合

ファイル拡張子に気をつけてください。

typebook/deepReadonly.test.ts
import type { DeepReadonly } from 'utility/types/deepReadonly';

type DeepArr = [number[], string[]];

test('ふつうの Readonly<T>', () => {
    type ReadonlyArr = Readonly<DeepArr>;
    const arr: ReadonlyArr = [[], []];
    // @ts-expect-error
    arr[0] = [];
    arr[1][0] = 'str';
    // @ts-expect-error
    arr.push(['str']);
    arr[0].push(2);
    arr[0][1] = 3;
});

test('DeepReadonly<T>', () => {
    type DeepReadonlyArr = DeepReadonly<DeepArr>;
    const arr: DeepReadonlyArr = [[], []];
    // @ts-expect-error
    arr1[0] = [];
    // @ts-expect-error
    arr1[1][0] = 'str';
    // @ts-expect-error
    arr1.push(['str']);
    // @ts-expect-error
    arr1[0].push(2);
    // @ts-expect-error
    arr1[0][1] = 3;
});

簡単ですね。

2-2-2. TypeScript 用テストライブラリを導入したくない場合

私は存じ上げませんが、さまざまな事情により、TypeScript 用のテストライブラリを導入したくない場合というのがあります。

その場合は普通にコンパイルしてしまいます。

package.json
   "scripts": {
+    "type-test": "tsc --noEmit ./typeBook"

ついでに GitHub Actions や Circle CI を使って自動テスト化してみましょう。

GitHub Actions のサンプルコードを置いておきます。

.github/workflows/type-test.yaml
name: Type Test
on: [pull_request]
jobs:
  lint:
    runs-on: macos-10.15
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '14'
          check-latest: true
      - uses: actions/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.OS }}-node-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-node-
            ${{ runner.OS }}-
      - name: npm install
        run: npm ci
      - name: Type Test
        run: npm run type-test

3. まとめ

  • TypeScript の型関数は StoryBook 的に使用例やエラー例を列挙すると挙動を把握しやすい
  • エラーは @ts-expect-error で明示する
  • 使用例を test 関数でラッピングするだけでテストになる
  1. 引用元『TypeScriptの型で遊ぶ時、再帰制限を(合法的に)突破する

  2. 引用元(孫引き)『conditional typeによるDeepReadonly<T> - TypeScriptの型入門

  3. StoryBook とは、UI コンポーネントの見本(sample book)を作るライブラリです。React/Next, Vue/Nuxt, Svelte/Sapper など幅広く対応しています。

  4. @ts-expect-error コメントでは、@ts-ignore コメントと違ってエラーがない場合エラーになります(公式リリースノート:Documentation - TypeScript 3.9

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?