jest-community/jest-extended

Type definitions not working

webstackdev opened this issue ยท 16 comments

  • package version: jest-extended 3.1.0
  • node version: v17.5.0
  • npm (or yarn) version: yarn 3.2.2
  • typescript: 4.8.4

I followed the instructions to use jest-extended with TypeScript shown on the project home page:

// global.d.ts
import 'jest-extended'

I also tried the triple slash directive using types as suggested in the documentation if the above doesn't work, and tried using a triple slash directive with a path directive to directly import the types from node_modules//jest-extended/types/index.d.ts.

I get the following error message:

test/jest/__tests__/jsdom-env/state.spec.ts:33:18 - error TS2339: Property 'toBeTrue' does not exist on type 'Matchers<void> & SnapshotMatchers<void, boolean> & Inverse<JestMatchers<void, boolean>> & PromiseMatchers<boolean>'.

    33     expect(true).toBeTrue()

I am certain my types directory is being included. I have this typeRoots directive in my common TypeScript config:

  "typeRoots": ["./@types/**/*.d.ts", "./node_modules/@types/**/*.d.ts"],

I also have a custom matcher with a type definition in my global @types folder, and it works fine:

// @types/jest/globals/extend.d.ts
declare module 'expect' {
  export interface Matchers<R> {
    toHaveInProtoChain(...chain: Constructor[]): R
  }
}

I noticed that the signature for my (working) custom matcher is different than the type provided by jest-extended:

declare namespace jest {
  interface Matchers<R> {
    ...

Any help in figuring out why I can't use the Jest extended matchers is greatly appreciated!

We need more information about your setup. Are you using ts-jest?

I have the same problem. I don't see how the type declarations (which obviously need to work through TS's declaration merging) of this package could possibly work. But I guess I'm missing something because otherwise more people would complain about this issue. @webstackdev have you already found a solution?

I ended up adding the matchers I need as manual types:

// @types/@jest/globals/expect.d.ts
declare module 'expect' {
  export interface Matchers<R> {
    /**
     * jest-extended typings do not work, so used matchers are manually included:
     * https://github.com/jest-community/jest-extended/issues/447
     * https://github.com/jest-community/jest-extended/issues/408
     */
    toBeNil(): R
    toBeObject(): R
    ...
  }
}

And adding the @types directory to my tsconfig typeRoots config. I found this library that says that to ensure the global jest declaration is augmented correctly, the Jest setup file with the expect.extend('...') statement for the matchers should be included via your TypeScript configuration:

"include": ["config/jest/setupJest.ts", "..."],

I'm still hoping to find a better solution than adding manual types for the matchers. I haven't tried the approach mentioned above to see if it works though.

I'd love to help, but as I said, I need some more information about your setup. Are you using ts-jest? If you have a sample little project that reproduces this that'd be very helpful too.

@keeganwitt sorry I missed your comment offering to help some time ago. It's in a fairly complex repo - I've had a todo to break the testing out into its own package but haven't finished it yet. It is using ts-jest, with "module": "commonjs" in the tsconfig file, and a expect.extend statement in the Jest setupFilesAfterEnv file that is including the jest-extended matchers I'm using.

I'd love to help, but as I said, I need some more information about your setup. Are you using ts-jest? If you have a sample little project that reproduces this that'd be very helpful too.

Thanks for your reply. No we are not using ts-jest in the project. Is that a problem? At the moment I unfortunately can't provide a little sample project as this issue happens with a bigger commercial project.

Hi, I got the issue when I start importing expect from @jest/globals.

import {describe, expect, test} from '@jest/globals'

It works fine if I use @types/jest and don't import directly from @jest/globals

Here is a mini repo to reproduce the issue. https://github.com/Yupeng-li/jest-extended-type-issure-mini-repo

I was having the same issue that was fixed by using jest globals instead of importing from @jest/globals like @Yupeng-li said.

However, I was able to work around it with the following declaration file:

// jest-extended.d.ts

import * as matchers from "jest-extended";

declare module "expect" {
	type JestExtendedMatchers = typeof matchers;`
	export interface Matchers<R> extends JestExtendedMatchers {}
}

It appears jest-extended extends the Matchers interface in the jest namespace, but the matchers aren't actually defined in that namespace when imported from @jest/globals.

Thank you @Blond11516 for the workaround.

If anyone is using asymmetric matches, such as expect.toBeNumber(), you need to add AsymmetricMatchers interface as well.

    interface AsymmetricMatchers extends JestExtendedMatchers { }

Also, there is a type issue when we use jest-extended matchers directly. The second param of toHaveBeenCalledBefore/toHaveBeenCalledAfter should be optional but it is not. #651

This is the new way matchers need to be added to be picked up by @jest/globals:

declare module '@jest/expect' {
  interface Matchers<R> {
    toHaveSortedColumn(columnTitle: string): R;
  }
}

So a workaround is:

import * as matchers from 'jest-extended';

type JestExtendedMatchers = typeof matchers;

declare module '@jest/expect' {
  interface Matchers<R> extends JestExtendedMatchers {}
}

Thanks @Blond11516 for the base code!

After analyzing this more deeply I can confirm that the problem was that ESLint has a conflict with the global typings of Jest and Cypress. As soon as ESLint comes across any file that imports something from Cypress it also loads the types of Cypress and from that moment on ESLint is confused and starts reporting errors.

So the obvious solution is to exclude any Cypress file from ESLint when it parses Jest test files that rely on the types of Jest and Jest-Extended. Then you can create a separate ESLint config (with overrides) for your Cypress tests. Once you do this you don't even have to import the Jest types any longer from Jest/Globals.

This way creating a separate module declaration for the matchers as described by @Blond11516 and @probablyup is not necessary. You can then simply use import "jest-extended"; to have the Jest-Extended types merged with the Jest types.

@krisztianb I'm not sure we're talking about the exact same issue. I don't have Cypress in my project, so at least in my case I can't be having conflicts between it and Jest.

Also, I'm not having issues with ESLint throwing errors, I simply can't get Typescript itself to compile without the workaround I shared.

Ditto, no Cypress here, just trying to switch from using the global Jest types to @jest/globals instead.

Sorry for the confusion guys.

Currently what works for me (TS wise) is:

  • Dont' import anything from @jest/globals in a test file.
  • Do an import "jest-extended"; in each file I want do use the extended matchers. The tripple slash directive works too instead of an import.
  • However the global.d.ts solution described on this page doesn't work for me. For some reason the types are not picked up. Which is a pitty because I would prefer not to import the types in every test file.

What doesn't work is what you seem to describe:

  • Import expect from @jest/globals in a test file like this: import { expect } from "@jest/globals";
  • Use an additional declaration file to extend the Jest types with the ones from jest-extended

I keep getting this error message:

Property 'toContainKeys' does not exist on type 'JestExpect'.ts(2339)

I did it like this:

  • Created a jest-extended.d.ts in the root of the TS project.
  • Include that file in tsconfig.json like so: "include": ["folder1", "folder2", "folder3", "jest-extended.d.ts"]
  • And the jest-extended.d.ts file has this content:
import * as matchers from "jest-extended";

type JestExtendedMatchers = typeof matchers;

declare module "@jest/expect" {
    interface Matchers<R> extends JestExtendedMatchers {}
}

Am I missing anyhting?

@krisztianb My best guess would be an issue with your TS configuration. In my case, my jest-extended.d.ts is under my rootDir, I'm not quite sure how include works.

If you add another type in your file, is it available from your test files? Here's an example:

// jest-extended.d.ts
import * as matchers from "jest-extended";

type JestExtendedMatchers = typeof matchers;

declare module "@jest/expect" {
    interface Matchers<R> extends JestExtendedMatchers {}
}

declare module "jest" {
    interface SomeType {}
}

Then in a test file:

// some/test/file.test.ts
import type { SomeType } from "jest";

const t: SomeType;

If this doesn't compile, it means jest-extended.d.ts isn't getting picked up by typescript.

@Blond11516 thank you for the help.

I can import SomeType the way you showed in my test files without error. However I still get this error message when accessing anything from jest-expect:

Property 'toContainKeys' does not exist on type 'JestExpect'.

One thing that is special in my case is that I must import from @jest/globals because in our project we are using cypress (that uses Chai) which also has an expect object with different methods.

Update

I found the problem. The issue was that our test folders were excluded in tsconfig so that they are not compiled. After I removed that exclude the following content of jest-extended.d.ts works now for me too:

import * as matchers from "jest-extended";

declare module "expect" {
    type JestExtendedMatchers = typeof matchers;

    export interface AsymmetricMatchers extends JestExtendedMatchers {}

    export interface Matchers<R> extends JestExtendedMatchers {}
}