关于 test-your-types
这项技能倡导为 TypeScript 类型编写测试,适合库作者和复杂类型工具的开发者,用于验证有效与无效用例、错误提示,以及类型声明与实现是否一致。
主要功能
- 01测试有效和无效类型
- 02检查预期的编译错误
- 03测试类型工具和边界情况
- 04使用 tsd、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确认已加载。
详细介绍
为类型编写测试
概述
正如你会为运行时代码编写测试,也应该为类型编写测试。类型级代码也可能有 bug,类型声明也可能与实现脱节。测试类型可确保声明正常工作,并能捕获应当报错的情况。
对于库作者、复杂类型工具,以及类型与实现分开定义的情况,类型测试尤为重要。
何时使用此技能
- 为库编写类型声明
- 创建复杂的类型工具
- 类型和实现位于不同文件
- 重构类型级代码
- 类型中包含条件逻辑或递归
铁律
为你的类型编写测试。测试有效类型是否正常工作、无效类型是否报错,以及错误消息是否有帮助。
检测
留意以下情况:
// 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
测试内容
测试以下三点:
- 有效类型正常工作 - 生成预期的类型
- 无效类型会报错 - 在预期的位置出现类型错误
- 错误消息有帮助 - 错误消息能引导用户修复问题
使用 @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
抗压协议
当有人施压要求跳过类型测试时:
- 说明风险:未经测试的类型可能存在细微 bug
- 从简单做起:很容易添加
@ts-expect-error测试 - 自动化:将类型测试添加到 CI 流水线
- 编写文档:测试可作为复杂类型的文档
危险信号
| 反模式 | 不良之处 |
|---|---|
| 不为复杂工具编写类型测试 | bug 会被忽视 |
| 只测试顺利路径 | 边界情况会出问题 |
| 类型与测试位于不同文件 | 两者可能脱节 |
| 在 IDE 中手动测试 | 无法复现 |
常见的自我安慰
“类型检查器会发现错误”
事实:类型检查器会根据你的类型进行验证,但不会验证你的类型是否正确。只有测试才能做到这一点。
“这只是类型,不是真正的代码”
事实:类型是在编译时运行的代码。复杂的类型逻辑和运行时逻辑一样需要测试。
“如果出了问题,我会注意到”
事实:类型 bug 很隐蔽。你可能要等到用户报告问题才会发现。
快速参考
| 工具 | 最适合的用途 | 命令 |
|---|---|---|
| @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》第二版
- 条目 55:为你的类型编写测试
评论
支持 Markdown