Типизация кастомного матчера Jest, чтобы expect(x).toBeIsoDate() компилировался
Проект добавляет матчер toBeIsoDate(). Реализация регистрируется в рантайме и работает, но каждая точка вызова не компилируется: Property 'toBeIsoDate' does not exist on type 'JestMatchers<string>'.
Требования: зарегистрировать матчер и добиться, чтобы точка вызова проходила проверку типов без приведений и без правки собственных определений Jest внутри node_modules.
// setup.ts
expect.extend({
toBeIsoDate(received: string) {
const pass = !Number.isNaN(Date.parse(received));
return { pass, message: () => `expected ${received} to be an ISO date` };
},
});
// your code here
// должно проходить проверку типов в любом тестовом файле:
expect('2026-07-12').toBeIsoDate();
Допишите реализацию.
expect.extend регистрирует только реализацию; на уровень типов из неё не попадает ничего. Метод добавляется слиянием объявлений в интерфейс матчеров Jest — declare global { namespace jest { interface Matchers<R> { toBeIsoDate(): R } } }. Слитый член и даёт сборку.
- ✗Ожидать, что
expect.extendчто-то меняет на уровне типов — это лишь регистрация в рантайме - ✗Приводить тип в каждой точке вызова вместо одного слияния члена в интерфейс матчеров
- ✗Опустить
declare global, из-за чего дополнение остаётся локальным, и точка вызова всё ещё падает
- →Как обобщённый параметр
RвMatchers<R>заставляет матчер работать с.notи с async? - →Где должен лежать файл объявлений, чтобы дополнение подхватилось проверкой типов?
Решение
// jest.d.ts — рядом с тестами, внутри include в tsconfig
export {};
declare global {
namespace jest {
interface Matchers<R> {
toBeIsoDate(): R;
}
}
}
// с @jest/globals вместо глобалов дополняют модуль:
import type {} from '@jest/expect';
declare module '@jest/expect' {
interface Matchers<R> {
toBeIsoDate(): R;
}
}
expect('2026-07-12').toBeIsoDate(); // ✅
expect('nonsense').not.toBeIsoDate(); // ✅ .not работает благодаря параметру R
Почему это работает
expect.extend — чистый рантайм: он кладёт функцию в реестр матчеров Jest. Система типов об этом не узнаёт ничего, поэтому expect(...) по-прежнему возвращает интерфейс без вашего метода.
Тип чинится слиянием объявлений: Matchers<R> — открытый интерфейс, и второе объявление добавляет в него член вместо того, чтобы заменить его. После этого toBeIsoDate виден на результате expect во всём проекте — без единого приведения.
⚠️ Дополнять надо тот интерфейс, который реально использует ваша сборка: при глобальных expect/describe — jest.Matchers, при явном импорте из @jest/globals — Matchers из модуля @jest/expect. Правка не того интерфейса компилируется, но на точку вызова не влияет.