TypeScript编译器无法识别Vitest自定义扩展匹配器求助
问题
我有一个TypeScript项目,单元测试使用Vitest扩展匹配器,测试可正常运行,但执行lint或TypeScript编译时会报错:
src/utils/tests/dateFilterUtils.test.ts:75:9 - error TS2339: Property 'toHaveMessage' does not exist on type 'Assertion<DateInputErrorI, any>'.
自定义匹配器代码如下:
export const addCustomMatchers = (): void => { expect.extend({ /** * 接收带message属性的对象,当message为非空字符串时测试通过 */ toHaveMessage(received: { message: string }) { return { pass: typeof received?.message === 'string' && received?.message?.length > 0, message: () => this.utils.matcherHint('toHaveMessage', undefined, '', { isNot: this.isNot, } as any) + '\n\n' + `Received: ${this.utils.printReceived(received)}`, }; }, }); };
我按照Vitest文档添加了vitest.d.ts文件,内容如下:
interface CustomMatchers<R = unknown> { toHaveMessage: () => R; } declare module 'vitest' { type Assertion<T = any> = CustomMatchers<T>; interface AsymmetricMatchersContaining extends CustomMatchers {} }
已在tsconfig.json的files字段中引用该文件,单元测试可正常运行,但TypeScript编译仍报错。
补充测试文件dateFilterUtils.test.ts代码:
import { addHours, startOfToday, subHours } from 'date-fns'; import { useDateInputFields, validateDateInputFields } from '../dateFilterUtils'; import { describe, test, expect } from 'vitest'; const getMockDateInputFieldsValue = (date: Date): ReturnType<typeof useDateInputFields> => { return { fullDate: date, year: date.getFullYear().toString(), month: (date.getMonth() + 1).toString(), day: date.getDate().toString(), time: `${date.getHours().toString()}:${date.getMinutes().toString()}`, } as ReturnType<typeof useDateInputFields>; }; describe('dateFilterUtils', () => { describe('validateDateInputFields', () => { // 测试用的模拟日期输入字段值 const empty = { fullDate: null, year: '', month: '', day: '', time: '', } as ReturnType<typeof useDateInputFields>; const future = getMockDateInputFieldsValue(new Date(2999, 2, 21, 12)); const today = getMockDateInputFieldsValue(addHours(startOfToday(), 2)); // 注意:假设测试不会在00:00-02:00之间运行 const yesterday = getMockDateInputFieldsValue(subHours(startOfToday(), 2)); const past = getMockDateInputFieldsValue(new Date(2007, 2, 21, 12)); test('日期无效时返回错误', () => { expect(validateDateInputFields(empty)).not.toHaveMessage(); expect(validateDateInputFields(past)).not.toHaveMessage(); expect(validateDateInputFields({ ...past, fullDate: null })).toHaveMessage(); }); test('日期应早于当日开始时间时返回错误', () => { expect(validateDateInputFields(future)).not.toHaveMessage(); expect(validateDateInputFields(future, { mustBeBeforeStartOfToday: true })).toHaveMessage(); expect(validateDateInputFields(today, { mustBeBeforeStartOfToday: true })).toHaveMessage(); expect( validateDateInputFields(yesterday, { mustBeBeforeStartOfToday: true }) ).not.toHaveMessage(); expect(validateDateInputFields(past, { mustBeBeforeStartOfToday: true })).not.toHaveMessage(); }); test('日期应晚于当日开始时间时返回错误', () => { expect(validateDateInputFields(yesterday)).not.toHaveMessage(); expect(validateDateInputFields(future, { mustBeTodayOrInFuture: true })).not.toHaveMessage(); expect(validateDateInputFields(today, { mustBeTodayOrInFuture: true })).not.toHaveMessage(); expect(validateDateInputFields(yesterday, { mustBeTodayOrInFuture: true })).toHaveMessage(); expect(validateDateInputFields(past, { mustBeTodayOrInFuture: true })).toHaveMessage(); }); test('日期应在未来时返回错误', () => { expect(validateDateInputFields(past)).not.toHaveMessage(); expect(validateDateInputFields(future, { mustBeInFuture: true })).not.toHaveMessage(); expect(validateDateInputFields(today, { mustBeInFuture: true })).toHaveMessage(); expect(validateDateInputFields(yesterday, { mustBeInFuture: true })).toHaveMessage(); expect(validateDateInputFields(past, { mustBeInFuture: true })).toHaveMessage(); }); test('日期应晚于起始日期时返回错误', () => { expect( validateDateInputFields(past, { mustBeAfterDate: new Date(1900, 0) }) ).not.toHaveMessage(); expect(validateDateInputFields(past, { mustBeAfterDate: new Date(2999, 0) })).toHaveMessage(); }); test('日期应等于或晚于起始日期时返回错误', () => { expect( validateDateInputFields(past, { mustBeEqualOrAfterDate: new Date(2007, 2, 21, 12) }) ).not.toHaveMessage(); expect( validateDateInputFields(past, { mustBeEqualOrAfterDate: new Date(2999, 0) }) ).toHaveMessage(); }); test('时间非可选且为空时返回错误', () => { const date = getMockDateInputFieldsValue(new Date(2999, 2, 21)); date.time = ''; expect(validateDateInputFields(date, { isTimeOptional: true })).not.toHaveMessage(); expect(validateDateInputFields(date, { isTimeOptional: false })).toHaveMessage(); }); }); });
解决方案
问题出在你对Vitest类型系统的扩展方式错误:你用type Assertion<T = any> = CustomMatchers<T>直接覆盖了Vitest原生的Assertion类型,而非扩展它。TypeScript中类型别名无法合并,只有接口可以合并,因此原生Assertion类型被覆盖后,自定义匹配器并未正确挂载到原本的断言类型上。
修复步骤如下:
- 修改
vitest.d.ts文件,改为扩展Vitest的接口而非覆盖:
interface CustomMatchers<R = unknown> { toHaveMessage: () => R; } declare module 'vitest' { // 扩展原生Assertion接口,而非重新定义类型 interface Assertion<T = any> extends CustomMatchers<T> {} interface AsymmetricMatchersContaining extends CustomMatchers {} }
- 确保
tsconfig.json中正确包含vitest.d.ts,示例配置:
{ "files": [ "./vitest.d.ts" ], // 其他配置... }
- 重启TypeScript语言服务(或IDE),让类型定义生效。
修改后TypeScript即可正确识别自定义的toHaveMessage匹配器,编译和lint时不会再报错。
内容的提问来源于stack exchange,提问作者paddingtonMike
相关产品推荐
相关产品推荐

