问题 扩展全局公开的第三方模块


我正在尝试在Typescript中为Jest添加自定义匹配器。这工作正常,但我不能让Typescript识别扩展 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__/*"
  ]
}

在someTests.ts中,我收到错误

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

我已多次阅读Microsoft文档,但我无法弄清楚如何使用全局可用类型(未导出)进行命名空间合并。

将它放在来自jest的index.d.ts中工作正常,但对于快速变化的代码库和多方扩展的类不是一个好的解决方案。


8745
2018-04-27 20:09


起源



答案:


好的,这里有几个问题

当源文件(.ts 要么 .tsx)文件和声明文件(.d.ts)文件都是模块解析的候选者,就像这里的情况一样,编译器将解析源文件。

您可能有两个文件,因为您要导出值并修改全局对象 jest。但是,您不需要两个文件,因为TypeScript具有一个特定的构造,用于从模块中扩充全局范围。也就是说,您需要的只是以下内容 .ts 文件

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 ? '!' : '='}==`
  }
}

也就是说,如果你有这样一个模块,那么执行全局是一个好习惯 突变 和全球类型 增强 在同一个文件中。鉴于此,我会考虑将其重写如下

myMatcher.ts

// no declare global wrapper because this is not a module
declare 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');
});

8
2018-04-28 07:51



在这里,我认为类型声明可以从.ts文件中移出到.d.ts文件中以供组织。实际上,.d.ts文件只是简单javascript的TS外观。对那里的普通进口也很好。有趣的是,如果我将declare语句放在someTest.ts文件中,我将丢失其余的Matchers声明信息。因此,TS编译器必须使用声明顺序做一些奇怪的事情,我将不得不进一步研究。 - Dylan Stewart
@DylanStewart声明可以用来计算 .ts 文件到 .d.ts 文件,但仅当它们具有不同的模块说明符时。例如。 my-module.ts 和 my-module-declarations.d.ts 没问题但是 my-module.ts 和 my-module.d.ts 是 不 好。同样的道理 .tsx 文件。 - Aluan Haddad
@AluanHaddad我遇到了你在那里提出的解决方案的问题。如果我使用声明全局方法它是有效的,但是如果我将declare namespace jest分解为一个单独的模块然后导入该模块它似乎不起作用。这是一个截图。你有什么想法? d2ffutrenqvap3.cloudfront.net/items/2A1H3v0v2B3l3e1a3q06/... - Tony
@Tony一个文件 同 顶级 import 要么 export 是一个模块。要影响模块中的全局声明空间,您需要使用 declare global。你的 custom-matchers.ts 有一个顶级 import 这意味着它是一个模块,需要块declare global - Aluan Haddad
@Tony你的代码所做的是声明一个模块范围的命名空间,它没有任何关系 jest全球。 - Aluan Haddad


答案:


好的,这里有几个问题

当源文件(.ts 要么 .tsx)文件和声明文件(.d.ts)文件都是模块解析的候选者,就像这里的情况一样,编译器将解析源文件。

您可能有两个文件,因为您要导出值并修改全局对象 jest。但是,您不需要两个文件,因为TypeScript具有一个特定的构造,用于从模块中扩充全局范围。也就是说,您需要的只是以下内容 .ts 文件

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 ? '!' : '='}==`
  }
}

也就是说,如果你有这样一个模块,那么执行全局是一个好习惯 突变 和全球类型 增强 在同一个文件中。鉴于此,我会考虑将其重写如下

myMatcher.ts

// no declare global wrapper because this is not a module
declare 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');
});

8
2018-04-28 07:51



在这里,我认为类型声明可以从.ts文件中移出到.d.ts文件中以供组织。实际上,.d.ts文件只是简单javascript的TS外观。对那里的普通进口也很好。有趣的是,如果我将declare语句放在someTest.ts文件中,我将丢失其余的Matchers声明信息。因此,TS编译器必须使用声明顺序做一些奇怪的事情,我将不得不进一步研究。 - Dylan Stewart
@DylanStewart声明可以用来计算 .ts 文件到 .d.ts 文件,但仅当它们具有不同的模块说明符时。例如。 my-module.ts 和 my-module-declarations.d.ts 没问题但是 my-module.ts 和 my-module.d.ts 是 不 好。同样的道理 .tsx 文件。 - Aluan Haddad
@AluanHaddad我遇到了你在那里提出的解决方案的问题。如果我使用声明全局方法它是有效的,但是如果我将declare namespace jest分解为一个单独的模块然后导入该模块它似乎不起作用。这是一个截图。你有什么想法? d2ffutrenqvap3.cloudfront.net/items/2A1H3v0v2B3l3e1a3q06/... - Tony
@Tony一个文件 同 顶级 import 要么 export 是一个模块。要影响模块中的全局声明空间,您需要使用 declare global。你的 custom-matchers.ts 有一个顶级 import 这意味着它是一个模块,需要块declare global - Aluan Haddad
@Tony你的代码所做的是声明一个模块范围的命名空间,它没有任何关系 jest全球。 - Aluan Haddad


一个简单的方法是:

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);
});

1
2018-04-18 12:40