Skip to content

[Docs]: Incomplete custom/asymmetric matcher documentation when using Jest globals and .d.ts files #15709

@dprevost-LMI

Description

@dprevost-LMI

Disclaimer: I'm trying out a bit more AI as we speak, and as we can clearly see below 😅

Page(s)

https://jestjs.io/docs/expect#expectextendmatchers

declare module 'expect' {
  interface AsymmetricMatchers {
    toBeWithinRange(floor: number, ceiling: number): void;
  }
  interface Matchers<R> {
    toBeWithinRange(floor: number, ceiling: number): R;
  }
}

tip
The type declaration of the matcher can live in a .d.ts file or in an imported .ts module (see JS and TS examples above respectively). If you keep the declaration in a .d.ts file, make sure that it is included in the program and that it is a valid module, i.e. it has at least an empty export {}.

Description

🔬 Minimal Reproduction

Repository: https://github.com/dprevost-LMI/jest-extend-repro

Setup:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  injectGlobals: true, // ← This is key
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
};
// jest.setup.ts
import type {MatcherFunction} from 'expect';

const toBeWithinRange: MatcherFunction<[floor: unknown, ceiling: unknown]> =
  function (actual, floor, ceiling) {
    // Implementation...
    const pass = actual >= floor && actual <= ceiling;
    return { pass, message: () => `...` };
  };

expect.extend({
  toBeWithinRange,
});

❌ Current Documentation Approach is generating confusion:

// types/jest-extensions.d.ts
declare module 'expect' {
  interface AsymmetricMatchers {
    toBeWithinRange(floor: number, ceiling: number): void;
  }
  interface Matchers<R> {
    toBeWithinRange(floor: number, ceiling: number): R;
  }
}

Result: TypeScript errors:

Property 'toBeWithinRange' does not exist on type 'InverseAsymmetricMatchers'.
Property 'toBeWithinRange' does not exist on type 'JestMatchers'.

✅ Working Solution:

// types/jest-extensions.d.ts
declare namespace jest {
  interface Matchers<R> {
    toBeWithinRange(floor: number, ceiling: number): R;
  }
  
  interface Expect {
    toBeWithinRange(floor: number, ceiling: number): any;
  }
  
  interface InverseAsymmetricMatchers {
    toBeWithinRange(floor: number, ceiling: number): any;
  }
}

🧐 Expected Behavior

The documentation should clearly explain:

  1. When to use declare module 'expect' vs declare namespace jest
  2. How injectGlobals: true affects type declarations
  3. Complete interface requirements for asymmetric matchers

📝 Current Documentation Issues

1. Missing InverseAsymmetricMatchers interface

The docs show AsymmetricMatchers but not InverseAsymmetricMatchers, which is required for expect.not.customMatcher().

2. Inconsistent module declaration approach

  • Shows declare module 'expect' but doesn't explain when this works vs when to use declare namespace jest
  • Doesn't mention that injectGlobals: true changes the type requirements

3. Incomplete TypeScript examples

  • Missing the Expect interface for asymmetric usage
  • No guidance on .d.ts file setup vs inline declarations

🌍 Environment

  • Jest version: 30.0.3
  • TypeScript version: 5.8.3
  • ts-jest version: 29.4.0
  • Configuration: injectGlobals: true, external .d.ts files

📚 Suggested Documentation Improvements

Add a section: "TypeScript Declarations for Different Setups"

Setup 1: Import-based approach with @jest/globals

import {expect} from '@jest/globals';

declare module 'expect' {
  interface AsymmetricMatchers {
    toBeWithinRange(floor: number, ceiling: number): void;
  }
  interface Matchers<R> {
    toBeWithinRange(floor: number, ceiling: number): R;
  }
}

Setup 2: Global Jest with external .d.ts files

// types/jest-extensions.d.ts
declare namespace jest {
  interface Matchers<R> {
    toBeWithinRange(floor: number, ceiling: number): R;
  }
  
  interface Expect {
    toBeWithinRange(floor: number, ceiling: number): any;
  }
  
  interface InverseAsymmetricMatchers {
    toBeWithinRange(floor: number, ceiling: number): any;
  }
}

Setup 3: Inline global declarations

declare global {
  namespace jest {
    interface Matchers<R> {
      toBeWithinRange(floor: number, ceiling: number): R;
    }
    // ... other interfaces
  }
}

🎯 Affected Documentation Pages

💡 Additional Context

This issue particularly affects developers who:

  • Use injectGlobals: true (the default behavior)
  • Organize TypeScript declarations in separate .d.ts files
  • Use asymmetric matchers in object matching scenarios
  • Follow modern Jest + TypeScript patterns

The current documentation leads to a confusing trial-and-error experience when the declare module 'expect' approach fails silently or with cryptic TypeScript errors.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions