Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending third party module that is globally exposed

I am trying to add a custom matcher to Jest in Typescript. This works fine, but I can't get Typescript to recognize the extended Matchers.

myMatcher.ts

export default function myMatcher (this: jest.MatcherUtils, received: any, expected: any): { pass: boolean; message (): string; } {   const pass = received === expected;   return {     pass: pass,     message: () => `expected ${pass ? '!' : '='}==`,   } } 

myMatcher.d.ts

declare namespace jest {   interface Matchers {     myMatcher (expected: any): boolean;   } } 

someTest.ts

import myMatcher from './myMatcher';  expect.extend({   myMatcher, })  it('should work', () => {   expect('str').myMatcher('str'); }) 

tsconfig.json

{   "compilerOptions": {     "outDir": "./dist/",     "moduleResolution": "node",     "module": "es6",     "target": "es5",     "lib": [       "es7",       "dom"     ]   },   "types": [     "jest"   ],   "include": [     "src/**/*"   ],   "exclude": [     "node_modules",     "dist",     "doc",     "**/__mocks__/*",     "**/__tests__/*"   ] } 

In someTests.ts, I get the error

error TS2339: Property 'myMatcher' does not exist on type 'Matchers' 

I have read through the Microsoft documentation several times, but I can't figure out how to do the namespace merging with globally available types (not exported).

Putting it in the index.d.ts from jest works fine, but isn't a good solution for a rapidly changing codebase and classes being extended by multiple parties.

like image 369
Dylan Stewart Avatar asked Apr 27 '17 20:04

Dylan Stewart


2 Answers

OK, so there are a few issues here

When a source file (.ts or .tsx) file and a declaration file (.d.ts) file are both candidates for module resolution, as is the case here, the compiler will resolve the source file.

You probably have two files because you want to export a value and also modify the type of the global object jest. However, you do not need two files for this as TypeScript has a specific construct for augmenting the global scope from within a module. That is to say, all you need is the following .ts file

myMatcher.ts

// use declare global within a module to introduce or augment a global declaration. declare global {   namespace jest {     interface Matchers {       myMatcher: typeof myMatcher;     }   } } export default function myMatcher<T>(this: jest.MatcherUtils, received: T, expected: T) {   const pass = received === expected;   return {     pass,     message: () => `expected ${pass ? '!' : '='}==`   }; } 

That said, if you have such a situation, it is a good practice to perform the global mutation and the global type augmentation in the same file. Given that, I would consider rewriting it as follows

myMatcher.ts

// ensure this is parsed as a module. export {};  declare global {   namespace jest {     interface Matchers {       myMatcher: typeof myMatcher;     }   } } function myMatcher<T>(this: jest.MatcherUtils, received: T, expected: T) {   const pass = received === expected;   return {     pass,     message: () => `expected ${pass ? '!' : '='}==`   }; }  expect.extend({   myMatcher }); 

someTest.ts

import './myMatcher';  it('should work', () => {   expect('str').myMatcher('str'); }); 
like image 64
Aluan Haddad Avatar answered Oct 18 '22 23:10

Aluan Haddad


A simple way is:

customMatchers.ts

declare global {     namespace jest {         interface Matchers<R> {             // add any of your custom matchers here             toBeDivisibleBy: (argument: number) => {};         }     } }  // this will extend the expect with a custom matcher expect.extend({     toBeDivisibleBy(received: number, argument: number) {         const pass = received % argument === 0;         if (pass) {             return {                 message: () => `expected ${received} not to be divisible by ${argument}`,                 pass: true             };         } else {             return {                 message: () => `expected ${received} to be divisible by ${argument}`,                 pass: false             };         }     } }); 

my.spec.ts

import "path/to/customMatchers";  test('even and odd numbers', () => {    expect(100).toBeDivisibleBy(2);    expect(101).not.toBeDivisibleBy(2); }); 
like image 42
shusson Avatar answered Oct 19 '22 00:10

shusson