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。
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 を自動テストにする
さて、これを自動テストにしていきましょう。
といっても、すでに mocha
や jest
等を用いて適切に .ts
ファイルの自動テストが適切に設定されていれば、とても簡単です。
そうでない場合もテストライブラリなしで書けます。
2-2-1. すでに ts-jest
等を利用したテスト環境がある場合
ファイル拡張子に気をつけてください。
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 用のテストライブラリを導入したくない場合というのがあります。
その場合は普通にコンパイルしてしまいます。
"scripts": {
+ "type-test": "tsc --noEmit ./typeBook"
ついでに GitHub Actions や Circle CI を使って自動テスト化してみましょう。
GitHub Actions のサンプルコードを置いておきます。
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
関数でラッピングするだけでテストになる
-
引用元(孫引き)『conditional typeによる
DeepReadonly<T>
- TypeScriptの型入門』 ↩ -
StoryBook とは、UI コンポーネントの見本(sample book)を作るライブラリです。React/Next, Vue/Nuxt, Svelte/Sapper など幅広く対応しています。 ↩
-
@ts-expect-error
コメントでは、@ts-ignore
コメントと違ってエラーがない場合エラーになります(公式リリースノート:Documentation - TypeScript 3.9) ↩