jest-extended icon indicating copy to clipboard operation
jest-extended copied to clipboard

Typescript error when manually import the matchers

Open igorwessel opened this issue 3 years ago • 8 comments

Bug

jest-extended version: 1.2.0

Problem

Currently jest-extended types don't export any of the matchers, so it ends up causing an error with the typescript. Captura de tela de 2022-01-26 10-40-31

We managed to solve this by declaring a module in the types

declare module "jest-extended" {
	export function toBeAfter(received: Date, after: Date): jest.CustomMatcherResult;
}

But doing it this way brings another problem that would be related to types, all manual imports that we are not using will be typed and will cause runtime issues Captura de tela de 2022-01-26 10-52-37 Captura de tela de 2022-01-26 10-52-51

I did some tests to try to solve this problem and a possible solution, but it still wouldn't be a good solution for this, it would be to move each declaration type to its own folder and import the file directly from the dist

  1. get declaration type for matcher
      //@file: types/index.d.ts
      declare namespace jest {
        // noinspection JSUnusedGlobalSymbols
        interface Matchers<R> {
          /**
           * Note: Currently unimplemented
           * Passing assertion
           *
           * @param {String} message
           */
          pass(message: string): R;
         ...
        }
      }
    
  2. moved to src/matchers/pass/index.d.ts:
    // @file: src/matchers/pass/index.d.ts <- not need this line
    /// <reference types="jest" />
    
    declare global {
      namespace jest {
        interface Matchers<R> {
          /**
           * Note: Currently unimplemented
           * Passing assertion
           *
           * @param {String} message
           */
          pass(message?: string): never;
        }
        interface Expect {
          /**
           * Note: Currently unimplemented
           * Passing assertion
           *
           * @param {String} message
           */
          pass(message?: string): void;
        }
      }
    }
    
    export declare function pass(expected: string, message?: string): jest.CustomMatcherResult;
    
  3. use in setup.ts
    import { pass } from 'jest-extended/dist/matchers/pass'
    
    expect.extend({
      pass
    })
    

I couldn't find another solution for this, as the behavior of the typescript consists of every js file must accompany its declaration file, if we use for example the strategy of re-exporting all files in an index.js, and importing only what we need, The typescript will follow all re-exported files and find declaration files (*.d.ts), which will have declaration type that you didn't import and merges with global jest -> expect/matchers.

igorwessel avatar Jan 26 '22 14:01 igorwessel

I managed to make the manual import work with the correct types.

I separated the matcher declaration as above and added the declarations for the typescript to start understanding exports

  1. In src/matchers/index.d.ts
export { matcher } from './matcher'
//...same line as above for all matchers
  1. In src/index.d.ts
export * from './matchers';
  1. I created the file to extend the matcher
//@file: jest.setup.ts
import { toBeAfter } from "jest-extended";

expect.extend({
  toBeAfter,
});
  1. In tsconfig only added the matcher declaration that I extended
//@file: tsconfig.json
{
 ...,
 "files": ["./node_modules/jest-extended/dist/matchers/toBeAfter/index.d.ts"],
}

And the types worked perfectly, it just recognized the matcher type that I extended. The only problem with this solution is that I haven't found a better way to import the types, I made a branch with the separate types if you want to test it yourself.

igorwessel avatar Jan 28 '22 10:01 igorwessel

Another problem that I forgot to mention is that if you separate the types every time you add a new matcher, you would have to update the file src/matchers/index.d.ts.

We can fix this if we transform the lib into typescript and let it typescript issue the declarations, then each matcher.ts would have something like:

//@file: src/matchers/toBeArray/index.ts
import predicate from './predicate';

declare global {
  namespace jest {
    interface Matchers<R> {
      /**
       * Use `.toBeArray` when checking if a value is an `Array`.
       */
      toBeArray(): R;
    }

    interface Expect {
      /**
       * Use `.toBeArray` when checking if a value is an `Array`.
       */
      toBeArray(): void;
    }
  }
}

const passMessage =
  ({ matcherHint, printReceived }: jest.MatcherContext['utils'], received: unknown) =>
  () =>
    matcherHint('.not.toBeArray', 'received', '') +
    '\n\n' +
    'Expected value to not be an array received:\n' +
    `  ${printReceived(received)}`;

const failMessage =
  ({ matcherHint, printReceived }: jest.MatcherContext['utils'], received: unknown) =>
  () =>
    matcherHint('.toBeArray', 'received', '') +
    '\n\n' +
    'Expected value to be an array received:\n' +
    `  ${printReceived(received)}`;

export function toBeArray(this: jest.MatcherContext, expected: any): jest.CustomMatcherResult {
  const pass = predicate(expected);
  if (pass) {
    return { pass: true, message: passMessage(this.utils, expected) };
  }

  return { pass: false, message: failMessage(this.utils, expected) };
}

This matcher toBeArray transformed into typescript, brings this output to its declaration file. (we can say that it is the same as the one we would have to write manually)

//@file: dist/matchers/toBeArray/index.d.ts
/// <reference types="jest" />
declare global {
    namespace jest {
        interface Matchers<R> {
            /**
             * Use `.toBeArray` when checking if a value is an `Array`.
             */
            toBeArray(): R;
        }
        interface Expect {
            /**
             * Use `.toBeArray` when checking if a value is an `Array`.
             */
            toBeArray(): void;
        }
    }
}
export declare function toBeArray(this: jest.MatcherContext, expected: any): jest.CustomMatcherResult;

igorwessel avatar Jan 29 '22 09:01 igorwessel

I'm seeing this issue too, hope to see it resolved soon!

Edit: I've found that this work-around is working for me

dospunk avatar Feb 14 '22 17:02 dospunk

I have a minimal setup with no Jest config file and only one file of tests, and—going off the link from the comment above—I was able to get it running today by only:

  1. npm i -D jest-extended (v2.0.0)
  2. Adding the line "setupFilesAfterEnv": ["jest-extended/all"] to the jest object in my package.json
  3. Adding the line import 'jest-extended'; to the top of my tests.ts file

Given my setup, I did not need a global.d.ts file at all.

davidlav avatar Feb 15 '22 00:02 davidlav

I have a minimal setup with no Jest config file and only one file of tests, and—going off the link from the comment above—I was able to get it running today by only:

  1. npm i -D jest-extended (v2.0.0)
  2. Adding the line "setupFilesAfterEnv": ["jest-extended/all"] to the jest object in my package.json
  3. Adding the line import 'jest-extended'; to the top of my tests.ts file

Given my setup, I did not need a global.d.ts file at all.

I believe you are confusing the problem, what I'm saying is that have problems importing a matcher instead of importing them all.

igorwessel avatar Feb 15 '22 06:02 igorwessel

Yeah, apologies. I had been beating my head against my desk for so long just to get this library to work with TypeScript, period, that I wanted to leave some help anyone else that found their way here, and that comment above mine pointed me in a helpful direction. But you're right, you've got a more specific problem.

davidlav avatar Feb 15 '22 12:02 davidlav

I have a minimal setup with no Jest config file and only one file of tests, and—going off the link from the comment above—I was able to get it running today by only:

  1. npm i -D jest-extended (v2.0.0)
  2. Adding the line "setupFilesAfterEnv": ["jest-extended/all"] to the jest object in my package.json
  3. Adding the line import 'jest-extended'; to the top of my tests.ts file

Given my setup, I did not need a global.d.ts file at all.

This doesn't work when running jest in esm mode, as there is no global jest object

viceice avatar Apr 29 '22 06:04 viceice

Weird how import "jest-extended" doesn't work (get the original is not a function error).

Then import "jest-extended/all" doesn't work either. I get another error TypeError: Cannot redefine property: toBeEmpty.

How difficult can it be to import some functions 😆

will-molloy avatar Feb 23 '23 23:02 will-molloy