Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Export additional interfaces for CommonJS module (Typescript)

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.

like image 404
Brian Avatar asked Jul 30 '17 19:07

Brian


People also ask

How do I import an interface in typescript?

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.

How do I export a module in typescript?

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.

How do I use CommonJS with typescript?

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

How to re-exporting a default field in typescript?

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:


1 Answers

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

like image 129
Aluan Haddad Avatar answered Oct 26 '22 02:10

Aluan Haddad