Repository: chimurai/http-proxy-middleware (ts)

Issue:
[Feature Request] Include TypeScript declaration in npm package
Since this package is already written in TypeScript, why not include TypeScript declaration in the npm package?

You just need to add the following lines to `tsconfig.json`

```json
{
  "compilerOptions": {
    "declaration": true
  }
}
```

and `packacge.json`

```json
{
  "types": "./dist/index.d.ts"
}
```

You can keep the declaration up to date instead of requiring to update `@types/http-proxy-middleware` separately. 

Pull request description:
feat(typescript): types support
fixes #345

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7dd857..640f74e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## next
 
 - feat: async router ([#379](https://github.com/chimurai/http-proxy-middleware/issues/335)) ([LiranBri](https://github.com/LiranBri))
+- feat(typescript): types support ([#369](https://github.com/chimurai/http-proxy-middleware/pull/369))
 
 ## [v0.20.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.20.0)
 
diff --git a/package.json b/package.json
index 3490e3f..0403123 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,9 @@
 {
   "name": "http-proxy-middleware",
-  "version": "0.21.0-beta.1",
+  "version": "0.21.0-beta.2",
   "description": "The one-liner node.js proxy middleware for connect, express and browser-sync",
   "main": "dist/index.js",
+  "types": "dist/index.d.ts",
   "files": [
     "dist"
   ],
@@ -51,7 +52,6 @@
     "@commitlint/cli": "^8.0.0",
     "@commitlint/config-conventional": "^8.0.0",
     "@types/express": "^4.17.0",
-    "@types/http-proxy": "^1.17.0",
     "@types/is-glob": "^4.0.0",
     "@types/jest": "^24.0.15",
     "@types/lodash": "^4.14.136",
@@ -72,6 +72,7 @@
     "ws": "^7.1.0"
   },
   "dependencies": {
+    "@types/http-proxy": "^1.17.2",
     "http-proxy": "^1.17.0",
     "is-glob": "^4.0.1",
     "lodash": "^4.17.14",
diff --git a/src/config-factory.ts b/src/config-factory.ts
index 7240b50..6dfe632 100644
--- a/src/config-factory.ts
+++ b/src/config-factory.ts
@@ -1,7 +1,8 @@
-import * as _ from 'lodash';
-import * as url from 'url';
+import _ from 'lodash';
+import url from 'url';
 import { ERRORS } from './errors';
 import { getInstance } from './logger';
+import { Filter, Options } from './types';
 
 const logger = getInstance();
 
@@ -9,7 +10,7 @@ export function createConfig(context, opts?) {
   // structure of config object to be returned
   const config = {
     context: undefined,
-    options: {} as any
+    options: {} as Options
   };
 
   // app.use('/api', proxy({target:'http://localhost:9000'}));
@@ -55,7 +56,7 @@ export function createConfig(context, opts?) {
  * @param  {String}  context [description]
  * @return {Boolean}         [description]
  */
-function isStringShortHand(context) {
+function isStringShortHand(context: Filter) {
   if (_.isString(context)) {
     return !!url.parse(context).host;
   }
@@ -72,11 +73,11 @@ function isStringShortHand(context) {
  * @param  {*}       opts    [description]
  * @return {Boolean}         [description]
  */
-function isContextless(context, opts) {
+function isContextless(context: Filter, opts: Options) {
   return _.isPlainObject(context) && _.isEmpty(opts);
 }
 
-function configureLogger(options) {
+function configureLogger(options: Options) {
   if (options.logLevel) {
     logger.setLevel(options.logLevel);
   }
diff --git a/src/context-matcher.ts b/src/context-matcher.ts
index f04f936..a2173a9 100644
--- a/src/context-matcher.ts
+++ b/src/context-matcher.ts
@@ -1,7 +1,7 @@
-import * as isGlob from 'is-glob';
-import * as _ from 'lodash';
-import * as micromatch from 'micromatch';
-import * as url from 'url';
+import isGlob from 'is-glob';
+import _ from 'lodash';
+import micromatch from 'micromatch';
+import url from 'url';
 import { ERRORS } from './errors';
 
 export function match(context, uri, req) {
diff --git a/src/handlers.ts b/src/handlers.ts
index 591a1b9..db441e2 100644
--- a/src/handlers.ts
+++ b/src/handlers.ts
@@ -1,4 +1,4 @@
-import * as _ from 'lodash';
+import _ from 'lodash';
 import { getInstance } from './logger';
 const logger = getInstance();
 
diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts
index f3f6634..e3c9572 100644
--- a/src/http-proxy-middleware.ts
+++ b/src/http-proxy-middleware.ts
@@ -1,21 +1,23 @@
-import * as httpProxy from 'http-proxy';
-import * as _ from 'lodash';
+import express from 'express';
+import httpProxy from 'http-proxy';
+import _ from 'lodash';
 import { createConfig } from './config-factory';
 import * as contextMatcher from './context-matcher';
 import * as handlers from './handlers';
 import { getArrow, getInstance } from './logger';
 import * as PathRewriter from './path-rewriter';
 import * as Router from './router';
+import { Filter, IRequest, IRequestHandler, IResponse, Options } from './types';
 
 export class HttpProxyMiddleware {
   private logger = getInstance();
   private config;
   private wsInternalSubscribed = false;
-  private proxyOptions;
-  private proxy;
+  private proxyOptions: Options;
+  private proxy: httpProxy;
   private pathRewriter;
 
-  constructor(context, opts) {
+  constructor(context: Filter | Options, opts?: Options) {
     this.config = createConfig(context, opts);
     this.proxyOptions = this.config.options;
 
@@ -45,7 +47,11 @@ export class HttpProxyMiddleware {
   }
 
   // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this
-  public middleware = async (req, res, next) => {
+  public middleware: IRequestHandler = async (
+    req: IRequest,
+    res: IResponse,
+    next: express.NextFunction
+  ) => {
     if (this.shouldProxy(this.config.context, req)) {
       const activeProxyOptions = await this.prepareProxyRequest(req);
       this.proxy.web(req, res, activeProxyOptions);
@@ -55,7 +61,7 @@ export class HttpProxyMiddleware {
 
     if (this.proxyOptions.ws === true) {
       // use initial request to access the server object to subscribe to http upgrade event
-      this.catchUpgradeRequest(req.connection.server);
+      this.catchUpgradeRequest((req.connection as any).server);
     }
   };
 
@@ -68,7 +74,7 @@ export class HttpProxyMiddleware {
     }
   };
 
-  private handleUpgrade = async (req, socket, head) => {
+  private handleUpgrade = async (req: IRequest, socket, head) => {
     if (this.shouldProxy(this.config.context, req)) {
       const activeProxyOptions = await this.prepareProxyRequest(req);
       this.proxy.ws(req, socket, head, activeProxyOptions);
@@ -84,7 +90,7 @@ export class HttpProxyMiddleware {
    * @param  {Object} req     [description]
    * @return {Boolean}
    */
-  private shouldProxy = (context, req) => {
+  private shouldProxy = (context, req: IRequest) => {
     const path = req.originalUrl || req.url;
     return contextMatcher.match(context, path, req);
   };
@@ -97,7 +103,7 @@ export class HttpProxyMiddleware {
    * @param {Object} req
    * @return {Object} proxy options
    */
-  private prepareProxyRequest = async req => {
+  private prepareProxyRequest = async (req: IRequest) => {
     // https://github.com/chimurai/http-proxy-middleware/issues/17
     // https://github.com/chimurai/http-proxy-middleware/issues/94
     req.url = req.originalUrl || req.url;
@@ -133,7 +139,7 @@ export class HttpProxyMiddleware {
   };
 
   // Modify option.target when router present.
-  private applyRouter = async (req, options) => {
+  private applyRouter = async (req: IRequest, options) => {
     let newTarget;
 
     if (options.router) {
@@ -151,7 +157,7 @@ export class HttpProxyMiddleware {
   };
 
   // rewrite path
-  private applyPathRewrite = (req, pathRewriter) => {
+  private applyPathRewrite = (req: IRequest, pathRewriter) => {
     if (pathRewriter) {
       const path = pathRewriter(req.url, req);
 
@@ -166,10 +172,11 @@ export class HttpProxyMiddleware {
     }
   };
 
-  private logError = (err, req, res) => {
+  private logError = (err, req: IRequest, res: IResponse) => {
     const hostname =
       (req.headers && req.headers.host) || (req.hostname || req.host); // (websocket) || (node0.10 || node 4/5)
-    const target = this.proxyOptions.target.host || this.proxyOptions.target;
+    const target =
+      (this.proxyOptions.target as any).host || this.proxyOptions.target;
     const errorMessage =
       '[HPM] Error occurred while trying to proxy request %s from %s to %s (%s) (%s)';
     const errReference =
diff --git a/src/index.ts b/src/index.ts
index 2e51092..d4ec7d6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,9 @@
 import { HttpProxyMiddleware } from './http-proxy-middleware';
+import { Filter, Options } from './types';
 
-function proxy(context, opts) {
-  const { middleware } = new HttpProxyMiddleware(context, opts);
+function middleware(context: Filter | Options, options?: Options) {
+  const { middleware } = new HttpProxyMiddleware(context, options);
   return middleware;
 }
 
-export = proxy;
+export = middleware;
diff --git a/src/logger.ts b/src/logger.ts
index 6f7f381..50aece4 100644
--- a/src/logger.ts
+++ b/src/logger.ts
@@ -1,5 +1,5 @@
-import * as _ from 'lodash';
-import * as util from 'util';
+import _ from 'lodash';
+import util from 'util';
 
 let loggerInstance;
 
diff --git a/src/path-rewriter.ts b/src/path-rewriter.ts
index dd0aaed..160a7f6 100644
--- a/src/path-rewriter.ts
+++ b/src/path-rewriter.ts
@@ -1,4 +1,4 @@
-import * as _ from 'lodash';
+import _ from 'lodash';
 import { ERRORS } from './errors';
 import { getInstance } from './logger';
 const logger = getInstance();
diff --git a/src/router.ts b/src/router.ts
index 37200b9..fcee4dc 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -1,4 +1,4 @@
-import * as _ from 'lodash';
+import _ from 'lodash';
 import { getInstance } from './logger';
 const logger = getInstance();
 
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..7aef997
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,59 @@
+import express from 'express';
+import http from 'http';
+import httpProxy from 'http-proxy';
+import net from 'net';
+
+export interface IRequest extends express.Request {}
+export interface IResponse extends express.Response {}
+
+export interface IRequestHandler extends express.RequestHandler {
+  upgrade?: (req: IRequest, socket: net.Socket, head: any) => void;
+}
+
+export type Filter =
+  | string
+  | string[]
+  | ((pathname: string, req: IRequest) => boolean);
+
+export interface Options extends httpProxy.ServerOptions {
+  pathRewrite?:
+    | { [regexp: string]: string }
+    | ((path: string, req: IRequest) => string);
+  router?:
+    | { [hostOrPath: string]: string }
+    | ((req: IRequest) => string)
+    | ((req: IRequest) => Promise<string>);
+  logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'silent';
+  logProvider?(provider: LogProvider): LogProvider;
+
+  onError?(err: Error, req: IRequest, res: IResponse): void;
+  onProxyRes?(
+    proxyRes: http.ServerResponse,
+    req: IRequest,
+    res: IResponse
+  ): void;
+  onProxyReq?(
+    proxyReq: http.ClientRequest,
+    req: IRequest,
+    res: IResponse
+  ): void;
+  onProxyReqWs?(
+    proxyReq: http.ClientRequest,
+    req: IRequest,
+    socket: net.Socket,
+    options: httpProxy.ServerOptions,
+    head: any
+  ): void;
+  onOpen?(proxySocket: net.Socket): void;
+  onClose?(res: IResponse, socket: net.Socket, head: any): void;
+}
+
+interface LogProvider {
+  log: Logger;
+  debug?: Logger;
+  info?: Logger;
+  warn?: Logger;
+  error?: Logger;
+}
+
+type Logger = (...args: any[]) => void;
diff --git a/tsconfig.json b/tsconfig.json
index 5b7d0fd..3952861 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,6 +5,8 @@
     "module": "commonjs",
     "moduleResolution": "node",
     "target": "es2015",
+    "declaration": true,
+    "esModuleInterop": true
   },
   "include": ["./src"]
 }
diff --git a/yarn.lock b/yarn.lock
index 0b0cf96..797e9c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -494,10 +494,10 @@
     "@types/express-serve-static-core" "*"
     "@types/serve-static" "*"
 
-"@types/http-proxy@^1.17.0":
-  version "1.17.0"
-  resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.0.tgz#baf82ff6aa2723fd29f90e3ba1384e665006863e"
-  integrity sha512-l+s0IoxSHqhLFJPDHRfO235kgrCkvFD8JmdV/T9C4BKBYPIjrQopGFH4r7h2e3jQqgJRCthRCAZIxDoFnj1zwQ==
+"@types/http-proxy@^1.17.2":
+  version "1.17.2"
+  resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.2.tgz#3b7fb5365a00d47129967b0b2da51c2123692314"
+  integrity sha512-Qfb7batJJBlI8wcrd48vHpgsOOYzQQa+OZcaIz33jkJPe8A7KktAJFmRAiR42s5BfnErdlFnOyQucq2BKy/98g==
   dependencies:
     "@types/node" "*"
 


Test patch:
diff --git a/test/e2e/_utils.ts b/test/e2e/_utils.ts
index 5eaed46..d3e9241 100644
--- a/test/e2e/_utils.ts
+++ b/test/e2e/_utils.ts
@@ -1,4 +1,4 @@
-import * as express from 'express';
+import express from 'express';
 
 // tslint:disable-next-line: no-var-requires
 export const proxyMiddleware = require('../../dist/index');
diff --git a/test/e2e/express-router.spec.ts b/test/e2e/express-router.spec.ts
index 09f24c1..3913981 100644
--- a/test/e2e/express-router.spec.ts
+++ b/test/e2e/express-router.spec.ts
@@ -1,5 +1,5 @@
-import * as express from 'express';
-import * as http from 'http';
+import express from 'express';
+import http from 'http';
 import { proxyMiddleware as proxy } from './_utils';
 
 describe('Usage in Express', () => {
diff --git a/test/e2e/http-proxy-middleware.spec.ts b/test/e2e/http-proxy-middleware.spec.ts
index 196e0de..3211f79 100644
--- a/test/e2e/http-proxy-middleware.spec.ts
+++ b/test/e2e/http-proxy-middleware.spec.ts
@@ -1,4 +1,4 @@
-import * as http from 'http';
+import http from 'http';
 import { createServer, proxyMiddleware } from './_utils';
 
 describe('E2E http-proxy-middleware', () => {
diff --git a/test/e2e/path-rewriter.spec.ts b/test/e2e/path-rewriter.spec.ts
index 62449a6..4699ae0 100644
--- a/test/e2e/path-rewriter.spec.ts
+++ b/test/e2e/path-rewriter.spec.ts
@@ -1,4 +1,4 @@
-import * as http from 'http';
+import http from 'http';
 import { createServer, proxyMiddleware } from './_utils';
 
 describe('E2E pathRewrite', () => {
diff --git a/test/e2e/router.spec.ts b/test/e2e/router.spec.ts
index e42887e..bd89e5a 100644
--- a/test/e2e/router.spec.ts
+++ b/test/e2e/router.spec.ts
@@ -1,4 +1,4 @@
-import * as http from 'http';
+import http from 'http';
 import { createServer, proxyMiddleware } from './_utils';
 
 describe('E2E router', () => {
diff --git a/test/e2e/websocket.spec.ts b/test/e2e/websocket.spec.ts
index 092d32a..53fce0a 100644
--- a/test/e2e/websocket.spec.ts
+++ b/test/e2e/websocket.spec.ts
@@ -1,5 +1,5 @@
-import * as http from 'http';
-import * as WebSocket from 'ws';
+import http from 'http';
+import WebSocket from 'ws';
 // tslint:disable-next-line: no-duplicate-imports
 import { Server as WebSocketServer } from 'ws';
 import { createServer, proxyMiddleware } from './_utils';
diff --git a/test/types.spec.ts b/test/types.spec.ts
new file mode 100644
index 0000000..64a0fdb
--- /dev/null
+++ b/test/types.spec.ts
@@ -0,0 +1,154 @@
+import middleware from '../src';
+import { Options } from '../src/types';
+
+describe('http-proxy-middleware TypeScript Types', () => {
+  let options: Options;
+
+  beforeEach(() => {
+    options = {
+      target: 'http://example.org'
+    };
+  });
+
+  it('should create proxy with just options', () => {
+    const proxy = middleware(options);
+    expect(proxy).toBeDefined();
+  });
+
+  describe('HPM Filters', () => {
+    it('should create proxy with path filter', () => {
+      const proxy = middleware('/path', options);
+      expect(proxy).toBeDefined();
+    });
+
+    it('should create proxy with glob filter', () => {
+      const proxy = middleware(['/path/**'], options);
+      expect(proxy).toBeDefined();
+    });
+
+    it('should create proxy with custom filter', () => {
+      const proxy = middleware((path, req) => true, options);
+      expect(proxy).toBeDefined();
+    });
+
+    it('should create proxy with manual websocket upgrade function', () => {
+      const proxy = middleware((path, req) => true, options);
+      expect(proxy.upgrade).toBeDefined();
+    });
+  });
+
+  describe('http-proxy options', () => {
+    it('should extend from http-proxy options', () => {
+      options = {
+        target: 'http://example',
+        ws: true
+      };
+      expect(options).toBeDefined();
+    });
+  });
+
+  describe('http-proxy-middleware options', () => {
+    describe('pathRewrite', () => {
+      it('should have pathRewrite Type with table', () => {
+        options = { pathRewrite: { '^/from': '/to' } };
+        expect(options).toBeDefined();
+      });
+
+      it('should have pathRewrite Type with function', () => {
+        options = { pathRewrite: (path, req) => '/path' };
+        expect(options).toBeDefined();
+      });
+    });
+
+    describe('router', () => {
+      it('should have router Type with table', () => {
+        options = { router: { '^/from': '/to' } };
+        expect(options).toBeDefined();
+      });
+
+      it('should have router Type with function', () => {
+        options = { router: path => '/path' };
+        expect(options).toBeDefined();
+      });
+
+      it('should have router Type with async function', () => {
+        options = { router: async path => '/path' };
+        expect(options).toBeDefined();
+      });
+    });
+
+    describe('logLevel', () => {
+      it('should have logLevel Type', () => {
+        options = { logLevel: 'info' };
+        expect(options).toBeDefined();
+      });
+    });
+
+    describe('logProvider', () => {
+      it('should have logProvider Type', () => {
+        options = {
+          logProvider: currentProvider => {
+            return {
+              log: () => {
+                return;
+              },
+              debug: () => {
+                return;
+              },
+              info: () => {
+                return;
+              },
+              warn: () => {
+                return;
+              },
+              error: () => {
+                return;
+              }
+            };
+          }
+        };
+        expect(options).toBeDefined();
+      });
+    });
+
+    describe('HPM http-proxy events', () => {
+      it('should have onError type', () => {
+        // tslint:disable no-empty
+        options = { onError: (err, req, res) => {} };
+        expect(options).toBeDefined();
+      });
+
+      it('should have onProxyReq type', () => {
+        // tslint:disable no-empty
+        options = { onProxyReq: (proxyReq, req, res) => {} };
+        expect(options).toBeDefined();
+      });
+
+      it('should have onProxyRes type', () => {
+        // tslint:disable no-empty
+        options = { onProxyRes: (proxyRes, req, res) => {} };
+        expect(options).toBeDefined();
+      });
+
+      it('should have onProxyReqWs type', () => {
+        options = {
+          // tslint:disable no-empty
+          onProxyReqWs: (proxyReq, req, socket, opts, head) => {}
+        };
+        expect(options).toBeDefined();
+      });
+
+      it('should have onOpen type', () => {
+        // tslint:disable no-empty
+        options = { onOpen: proxySocket => {} };
+        expect(options).toBeDefined();
+      });
+
+      it('should have onClose type', () => {
+        // tslint:disable no-empty
+        options = { onClose: (res, socket, head) => {} };
+        expect(options).toBeDefined();
+      });
+    });
+  });
+});