You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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类型被覆盖后,自定义匹配器并未正确挂载到原本的断言类型上。

修复步骤如下:

  1. 修改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 {}
}
  1. 确保tsconfig.json中正确包含vitest.d.ts,示例配置:
{
  "files": [
    "./vitest.d.ts"
  ],
  // 其他配置...
}
  1. 重启TypeScript语言服务(或IDE),让类型定义生效。

修改后TypeScript即可正确识别自定义的toHaveMessage匹配器,编译和lint时不会再报错。

内容的提问来源于stack exchange,提问作者paddingtonMike

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 02:36:02