test-your-types について
TypeScript の型を実行時コードと同様にテストするためのスキルです。ライブラリー作者や複雑な型ユーティリティの開発者が、有効・無効なケースやエラー動作、実装とのずれを検証できます。
主な機能
- 01有効・無効な型のテスト
- 02想定されるコンパイルエラーの確認
- 03型ユーティリティと境界ケースのテスト
- 04tsd、expect-type、Vitest の利用
- 05型エラーメッセージの検証
活用シーン
- 01ライブラリーの型宣言のテスト
- 02複雑な型ユーティリティの検証
- 03型宣言と実装のずれの防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill test-your-typesインストール後、Claude Code で/test-your-typesを入力して読み込まれたか確認します。
詳細
型のテストを書く
概要
実行時コードのテストを書くのと同じように、型のテストも書くべきです。型レベルのコードにもバグがあり、型宣言が実装と一致しなくなることもあります。型をテストすることで、型宣言が正しく機能し、想定されるエラーを検出できるか確認できます。
型のテストは、ライブラリの作成者や複雑な型ユーティリティを扱う場合、また型を実装とは別に定義する場合に特に重要です。
このスキルを使うタイミング
- ライブラリの型宣言を書くとき
- 複雑な型ユーティリティを作成するとき
- 型と実装が別のファイルにあるとき
- 型レベルのコードをリファクタリングするとき
- 型に条件分岐や再帰が含まれるとき
絶対に守るべきルール
型のテストを書きましょう。有効な型が機能すること、無効な型がエラーになること、エラーメッセージが役に立つことをテストしてください。
検出
次の状況に注意してください。
// RED FLAGS - Untested type logic
type ComplexTransform<T> = /* 10 lines of conditional types */;
// No tests - how do you know it works?
declare function libraryFn<T>(input: T): SomeTransform<T>;
// Implementation in JS, types in d.ts - can drift apart
テストする内容
次の3点をテストします。
- 有効な型が機能する - 期待される型が生成される
- 無効な型がエラーになる - 想定される箇所で型エラーが発生する
- エラーメッセージが役に立つ - エラーが修正方法を示してくれる
@ts-expect-error を使ったテスト
@ts-expect-error を使って、行で型エラーが発生することを検証します。
// myFunction.test.ts
import { myFunction } from './myFunction';
// Test 1: Valid types work
const result1 = myFunction('hello');
type Test1 = typeof result1; // Should be string
// Test 2: Invalid types fail
// @ts-expect-error - number not assignable to string
const result2 = myFunction(42);
// Test 3: Error message is helpful
// When the error goes away, TypeScript warns:
// "Unused '@ts-expect-error' directive"
型ユーティリティのテスト
// type-utils.ts
export type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
// type-utils.test.ts
import type { DeepReadonly } from './type-utils';
// Test: Simple object
type Case1 = DeepReadonly<{ x: number }>;
const test1: Case1 = { x: 1 };
// @ts-expect-error - readonly
test1.x = 2;
// Test: Nested object
type Case2 = DeepReadonly<{ nested: { value: string } }>;
const test2: Case2 = { nested: { value: 'hi' } };
// @ts-expect-error - deeply readonly
// @ts-expect-error - deeply readonly
test2.nested.value = 'bye';
// Test: Arrays
type Case3 = DeepReadonly<string[]>;
const test3: Case3 = ['a', 'b'];
// @ts-expect-error - readonly array
test3.push('c');
expect-type を使ったテスト
expect-type ライブラリには、型アサーション用のヘルパーが用意されています。
import { expectType, expectError } from 'expect-type';
// Test return types
const result = myFunction('input');
expectType<string>(result);
// Test that errors occur
expectError(myFunction(42));
// Test complex types
interface User { name: string; }
const user = fetchUser();
expectType<User>(user);
tsd を使ったテスト
tsd は、型定義をテストする CLI ツールです。
// index.test-d.ts
import { expectType, expectError } from 'tsd';
import { concat } from '.';
// Test: string + string = string
expectType<string>(concat('foo', 'bar'));
// Test: number + number = number
expectType<number>(concat(1, 2));
// Test: mixed types = error
expectError(concat('foo', 1));
次のコマンドで実行します:npx tsd
Vitest を使ったテスト
Vitest には型テスト機能が組み込まれています。
// test/types.test-d.ts
import { describe, expectTypeOf, it } from 'vitest';
import { pick } from './utils';
describe('pick', () => {
it('should pick specified keys', () => {
const obj = { a: 1, b: 2, c: 3 };
const picked = pick(obj, 'a', 'b');
expectTypeOf(picked).toEqualTypeOf<{ a: number; b: number }>();
});
it('should not allow unpicked keys', () => {
const obj = { a: 1, b: 2 };
const picked = pick(obj, 'a');
// @ts-expect-error - 'b' was not picked
picked.b;
});
});
次のコマンドで実行します:npx vitest typecheck
エラーメッセージのテスト
わかりやすいエラーメッセージは API の一部です。
// Test that error messages are helpful
type Check<T> = T extends string ? T : never;
// Bad: Error is just "Type 'number' is not assignable to type 'never'"
type Bad = Check<number>;
// Better: Use meaningful type names
type CheckWithMessage<T> = T extends string
? T
: 'Error: Expected string, received something else';
エッジケースのテスト
// Test with unions
type UnionTest = MyType<string | number>;
// Should distribute: MyType<string> | MyType<number>
// Test with never
type NeverTest = MyType<never>;
// Should handle never gracefully
// Test with any
type AnyTest = MyType<any>;
// Should not crash or produce unexpected results
// Test with complex objects
type ComplexTest = MyType<{ a: { b: { c: string } } }>;
// Should handle nesting correctly
圧力に負けないためのプロトコル
型テストを省略するよう求められたときは、次のように対応します。
- リスクを示す:テストされていない型には、見つけにくいバグが潜む可能性があります
- 簡単なところから始める:
@ts-expect-errorを使ったテストは簡単に追加できます - 自動化する:CI パイプラインに型テストを追加します
- 文書化する:テストは複雑な型のドキュメントとして役立ちます
注意すべき兆候
| アンチパターン | 問題点 |
|---|---|
| 複雑なユーティリティに型テストがない | バグが見過ごされます |
| 正常系しかテストしていない | エッジケースで問題が発生します |
| 型とテストが別のファイルにある | 互いにずれていく可能性があります |
| IDE で手動テストしている | 再現できません |
よくある正当化
「型チェッカーがエラーを検出してくれる」
実際には:型チェッカーは型に照らして検証しますが、その型が正しいかどうかは検証しません。それを検証できるのはテストだけです。
「ただの型であって、本物のコードではない」
実際には:型はコンパイル時に実行されるコードです。複雑な型のロジックにも、実行時のロジックと同じようにテストが必要です。
「何かが壊れたら気づくはずだ」
実際には:型のバグは見つけにくいものです。ユーザーから問題が報告されるまで気づかないこともあります。
クイックリファレンス
| ツール | 最適な用途 | コマンド |
|---|---|---|
| @ts-expect-error | 手軽なテスト、インライン | 組み込み |
| expect-type | 単体テスト形式 | npm test |
| tsd | ライブラリの型定義 | npx tsd |
| Vitest | テストスイート全体 | npx vitest typecheck |
| dtslint | DefinitelyTyped | npx dtslint |
要点
型はコードであり、テストが必要です。手軽な確認には @ts-expect-error を使い、包括的なテストには専用ライブラリを使いましょう。有効なケース、無効なケース、エラーメッセージをテストしてください。
参考資料
- Dan Vanderkam 著『Effective TypeScript』第2版
- 項目55:型のテストを書く