I'm trying to use a simple JS library in Typescript/React, but am unable to create a definition file for it. The library is google-kgsearch (https://www.npmjs.com/package/google-kgsearch). It exports a single function in the CommonJS style. I can successfully import and call the function, but can't figure out how to reference the type of the arguments to the result callback.
Here is most of the library code:
function KGSearch (api_key) {
this.search = (opts, callback) => {
....
request({ url: api_url, json: true }, (err, res, data) => {
if (err) callback(err)
callback(null, data.itemListElement)
})
....
return this
}
}
module.exports = (api_key) => {
if (!api_key || typeof api_key !== 'string') {
throw Error(`[kgsearch] missing 'api_key' {string} argument`)
}
return new KGSearch(api_key)
}
And here is my attempt to model it. Most of the interfaces model the results returned by service:
declare module 'google-kgsearch' {
function KGSearch(api: string): KGS.KGS;
export = KGSearch;
namespace KGS {
export interface SearchOptions {
query: string,
types?: Array<string>,
languages?: Array<string>,
limit?: number,
maxDescChars?: number
}
export interface EntitySearchResult {
"@type": string,
result: Result,
resultScore: number
}
export interface Result {
"@id": string,
name: string,
"@type": Array<string>,
image: Image,
detailedDescription: DetailedDescription,
url: string
}
export interface Image {
contentUrl: string,
url: string
}
export interface DetailedDescription {
articleBody: string,
url: string,
license: string
}
export interface KGS {
search: (opts: SearchOptions, callback: (err: string, items: Array<EntitySearchResult>) => void) => KGS.KGS;
}
}
}
My issue is that from another file I am unable to reference the KGS.EntitySearchResult array returned by the search callback. Here is my use of the library:
import KGSearch = require('google-kgsearch');
const kGraph = KGSearch(API_KEY);
interface State {
value: string;
results: Array<KGS.EntitySearchResult>; // <-- Does not work!!
}
class GKGQuery extends React.Component<Props, object> {
state : State;
handleSubmit(event: React.FormEvent<HTMLFormElement>) {
kGraph.search({ query: this.state.value }, (err, items) => { this.setState({results: items}); });
event.preventDefault();
}
....
}
Any suggestions for how to make the result interfaces visible to my calling code without messing up the default export is very greatly appreciated.
TypeScript uses the concept of modules , in the same way that JavaScript does. In order to be able to import an interface from a different file, it has to be exported using a named or default export. The example above uses named exports and named imports.
TypeScript supports export =to model the traditional CommonJS and AMD workflow. The export =syntax specifies a single object that is exported from the module. This can be a class, interface, namespace, function, or enum. When exporting a module using export =, TypeScript-specific import module = require("module")must be used to import the module.
ES Module Syntax with CommonJS Behavior TypeScript has ES Module syntax which directly correlates to a CommonJS and AMD require. Imports using ES Module are for most cases the same as the require from those environments, but this syntax ensures you have a 1 to 1 match in your TypeScript file with the CommonJS output: import fs = require("fs");
default exports can also be just values: With TypeScript 3.8, you can use export * as ns as a shorthand for re-exporting another module with a name: This takes all of the dependencies from a module and makes it an exported field, you could import it like this:
The issue here is easily resolved. The problem is that while you have exported KGSearch
, you have not exported the namespace KGS
that contains the types. There are several ways to go about this, but the one I recommend is to take advantage of Declaration Merging
Your code will change as follows
declare module 'google-kgsearch' {
export = KGSearch;
function KGSearch(api: string): KGSearch.KGS;
namespace KGSearch {
// no changes.
}
}
Then from consuming code
import KGSearch = require('google-kgsearch');
const kGraph = KGSearch(API_KEY);
interface State {
value: string;
results: Array<KGSearch.EntitySearchResult>; // works!!
}
Unfortunately, whenever we introduce an ambient external module declaration, as we have by writing declare module 'google-kgsearch'
at global scope, we pollute the global namespace of ambient external modules (that is a mouthful I know). Although it is unlikely to cause a conflict in your specific project for the time being, it means that if someone adds an @types
package for google-kgsearch
and you have a dependency which in turn depends on this @types
package or if google-kgsearch
every starts to ship its own typings, we will run into errors.
To resolve this we can use a non-ambient module to declare our custom declarations but this involves a bit more configuration.
Here is how we can go about this
tsconfig.json
{
"compilerOptions": {
"baseUrl": "." // if not already set
"paths": { // if you already have this just add the entry below
"google-kgsearch": [
"custom-declarations/google-kgsearch"
]
}
}
}
custom-declarations/google-kgsearch.d.ts (name does not matter just needs to match paths)
// do not put anything else in this file
// note that there is no `declare module 'x' wrapper`
export = KGSearch;
declare function KGSearch(api: string): KGSearch.KGS;
declare namespace KGSearch {
// ...
}
This encapsulates us from version conflicts and transitive dependency issues by defining it as an external module instead of an ambient external module.
One last thing to seriously consider is sending a pull request to krismuniz/google-kgsearch that adds your typings (the second version) in a file named index.d.ts. Also, if the maintainers do not wish to include them, consider creating an @types/google-kgsearch
package by sending a pull request to DefinitelyTyped
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With