7

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.

1
  • Thumbs up for using import ... = require('...'); for a CommonJS module. That is a very good practice. Commented Jul 30, 2017 at 20:55

1 Answer 1

4

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

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you, this is an excellent answer. I didn't realize that by having the same name, the namespace would get merged into the exported function, but it works. I'll try your additional configuration suggestions as well.
Yeah, the reason it is necessary has to do with CommonJS modules and UMD style declarations. Otherwise you could export them separately. On the upside, this keeps imports clean for consumers.
I'll send a PR with the types once I've used them a bit more and have more confidence the api results really match my definitions. Thanks again :)
Awesome, the more the merrier. Glad I was helpful.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.