mcpField
T
test-your-types

test-your-types

github.com/marius-townhouse测试与质量643 次安装38 周前更新
代码仓库

关于 test-your-types

这项技能倡导为 TypeScript 类型编写测试,适合库作者和复杂类型工具的开发者,用于验证有效与无效用例、错误提示,以及类型声明与实现是否一致。

主要功能

  1. 01测试有效和无效类型
  2. 02检查预期的编译错误
  3. 03测试类型工具和边界情况
  4. 04使用 tsd、expect-type 或 Vitest
  5. 05验证类型错误提示

适用场景

  1. 01测试库的类型声明
  2. 02检查复杂类型工具
  3. 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

测试内容

测试以下三点:

  1. 有效类型正常工作 - 生成预期的类型
  2. 无效类型会报错 - 在预期的位置出现类型错误
  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

抗压协议

当有人施压要求跳过类型测试时:

  1. 说明风险:未经测试的类型可能存在细微 bug
  2. 从简单做起:很容易添加 @ts-expect-error 测试
  3. 自动化:将类型测试添加到 CI 流水线
  4. 编写文档:测试可作为复杂类型的文档

危险信号

反模式不良之处
不为复杂工具编写类型测试bug 会被忽视
只测试顺利路径边界情况会出问题
类型与测试位于不同文件两者可能脱节
在 IDE 中手动测试无法复现

常见的自我安慰

“类型检查器会发现错误”

事实:类型检查器会根据你的类型进行验证,但不会验证你的类型是否正确。只有测试才能做到这一点。

“这只是类型,不是真正的代码”

事实:类型是在编译时运行的代码。复杂的类型逻辑和运行时逻辑一样需要测试。

“如果出了问题,我会注意到”

事实:类型 bug 很隐蔽。你可能要等到用户报告问题才会发现。

快速参考

工具最适合的用途命令
@ts-expect-error快速测试、内联测试内置
expect-type单元测试风格npm test
tsd库类型定义npx tsd
Vitest完整测试套件npx vitest typecheck
dtslintDefinitelyTypednpx dtslint

总结

类型是代码,也需要测试。使用 @ts-expect-error 进行快速检查,使用专用库进行全面测试。测试有效情况、无效情况和错误消息。

参考资料

  • Dan Vanderkam 著《Effective TypeScript》第二版
  • 条目 55:为你的类型编写测试

评论

支持 Markdown