Typing a custom Jest matcher so that expect(x).toBeIsoDate() compiles
A project adds a matcher toBeIsoDate(). The implementation is registered at run time and works, but every call site fails to compile: Property 'toBeIsoDate' does not exist on type 'JestMatchers<string>'.
Requirements: register the matcher and make the call site type-check with no assertion, and without editing Jest's own type definitions inside 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
// must type-check in any test file:
expect('2026-07-12').toBeIsoDate();
Write the implementation.
expect.extend only registers the implementation; nothing about it reaches the type level. You add the method by declaration merging into Jest's matcher interface — declare global { namespace jest { interface Matchers<R> { toBeIsoDate(): R } } }. The merged member is what compiles.
- ✗Expecting
expect.extendto change anything at the type level — it is a runtime registration only - ✗Asserting at every call site instead of merging the member into the matcher interface once
- ✗Omitting
declare global, so the augmentation stays local to the file and the call site still errors
- →How does the generic parameter
RonMatchers<R>make the matcher work with.notand with async? - →Where must the declaration file live for the augmentation to be picked up by the type-check?
The solution
// jest.d.ts — beside the tests, inside the tsconfig include
export {};
declare global {
namespace jest {
interface Matchers<R> {
toBeIsoDate(): R;
}
}
}
// with @jest/globals you augment the module instead of the 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 works thanks to the R parameter
Why it works
expect.extend is pure runtime: it drops the function into Jest's matcher registry. The type system learns nothing from it, so expect(...) still returns an interface that has no such method.
The types are fixed by declaration merging: Matchers<R> is an open interface, and a second declaration adds a member to it rather than replacing it. After that, toBeIsoDate is visible on the result of expect across the whole project — with no assertion anywhere.
⚠️ Augment the interface your setup actually uses: with global expect/describe that is jest.Matchers; with an explicit import from @jest/globals it is Matchers from the @jest/expect module. Patching the wrong one compiles fine but has no effect on the call site.