Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript Express Api class is not a constructor when testing with ts-node

I have set up an express application using typescript classes and have run into a strange issue. All tests have been passing, and today when I went to update some of the routes, my tests no longer run. When I run my test script, I get this error message back in the console:

$ mocha -c --reporter spec --compilers ts:ts-node/register ./test/*.test.ts --
timeout 20000

/Users/christiantodd/Development/projects/bby-react-api/src/index.ts:8
const app: Api = new Api();
             ^
TypeError: Api_1.default is not a constructor
at Object.<anonymous> (/Users/christiantodd/Development/projects/bby-react-api/src/index.ts:8:18)
at Module._compile (module.js:635:30)
at Module.m._compile (/Users/christiantodd/Development/projects/bby-react-api/node_modules/ts-node/src/index.ts:392:23)
at Module._extensions..js (module.js:646:10)
at Object.require.extensions.(anonymous function) [as .ts] (/Users/christiantodd/Development/projects/bby-react-api/node_modules/ts-node/src/index.ts:395:12)

My Api.ts file looks as follows:

import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as expressValidator from 'express-validator';
import * as helmet from 'helmet';
import * as morgan from 'morgan';
import * as passport from 'passport';
import * as compression from 'compression';

/* import all routers */
import BestBuyRouter from './routes/BestBuyRouter';
import UserRouter from './routes/UserRouter';

export default class Api {
  /* reference to the express instance */
  public express: express.Application;

  /* create the express instance and attach app level middleware and routes */
  constructor() {
    this.express = express();
    this.middleware();
    this.routes();
  }

  /* get current environment */
  public currentEnv(): string {
    return this.express.get('env');
  }

  /* apply middleware */
  private middleware(): void {
    this.express.use((req, res, next) => {
      /* Don't allow caching. Needed for IE support :/ */
      res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
      res.header('Pragma', 'no-cache');
      res.header('Access-Control-Allow-Origin', '*');
      res.header(
        'Access-Control-Allow-Methods',
        'PUT, GET, POST, DELETE, OPTIONS'
      );
      res.header(
        'Access-Control-Allow-Headers',
        'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials'
      );
      res.header('Access-Control-Allow-Credentials', 'true');
      next();
    });
    this.express.use(compression());
    this.express.use(helmet());
    this.express.use(morgan('dev'));
    this.express.use(bodyParser.json());
    this.express.use(bodyParser.urlencoded({ extended: false }));
    this.express.use(passport.initialize());
    this.express.use(expressValidator());
    this.express.use((err, req, res, next) => {
      console.error(err);
      res.status(err.status || 500).json({
        message: err.message,
        error: err
      });
    });
  }

  /* connect resource routers */
  private routes(): void {
    /* create an instance of the each of our routers */
    const userRouter = new UserRouter();
    const bestBuyRouter = new BestBuyRouter();

    /* attach all routers to our express app */
    this.express.use(userRouter.path, userRouter.router);
    this.express.use(bestBuyRouter.path, bestBuyRouter.router);
  }
}

and my index.ts:

import Api from './Api';
require('dotenv').config();
const mongoose = require('mongoose');
/* Set mongoose promise to native ES6 promise */
mongoose.Promise = global.Promise;

/* Instantiate our app instance */
const app: Api = new Api();

const connectOptions = {
  useMongoClient: true,
  keepAlive: true,
  reconnectTries: Number.MAX_VALUE
};

/* Get current environment */
export const ENV = app.currentEnv();

let DATABASE_URL;
let PORT;

/* set environment variables */
if (ENV === 'production') {
  DATABASE_URL = process.env.MONGODB_URI;
  PORT = parseInt(process.env.PORT, 10);
} else {
  DATABASE_URL = process.env.TEST_DATABASE_URL;
  PORT = 3000;
}

let server;

export const runServer = async (
  dbURL: string = DATABASE_URL,
  port: number = PORT
) => {
  try {
    await mongoose.connect(dbURL, connectOptions);
    await new Promise((resolve, reject) => {
      server = app.express
        .listen(port, () => {
          console.info(`The ${ENV} server is listening on port ${port} 🤔`);
          resolve();
        })
        .on('error', err => {
          mongoose.disconnect();
          reject(err);
        });
    });
  } catch (err) {
    console.error(err);
  }
};

export const closeServer = async () => {
  try {
    await mongoose.disconnect();
    await new Promise((resolve, reject) => {
      console.info(`Closing server. Goodbye old friend.`);
      server.close(err => (err ? reject(err) : resolve()));
    });
  } catch (err) {
    console.error(err);
  }
};

require.main === module && runServer().catch(err => console.error(err));

Lastly, my tsconfig.json

{
  "compilerOptions": {
    "lib": ["dom", "es7"],
    "allowJs": true,
    "watch": true,
    "noImplicitAny": false,
    "removeComments": true,
    "sourceMap": false,
    "target": "es6",
    "module": "commonjs",
    "outDir": "./lib",
    "types": [
      "body-parser",
      "mongodb",
      "mongoose",
      "passport",
      "node",
      "nodemailer",
      "mocha",
      "chai",
      "express",
      "express-validator",
      "chai-http"
    ],
    "typeRoots": ["./node_modules/@types"]
  },
  "compileOnSave": true,
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "**/*.test.ts"]
}

To me it's really strange to get this behavior all of the sudden when this config has worked for me just fine in the past. I can still start my server just fine, but for some reason ts-node doesn't want to compile my *test.ts files for mocha to run my tests. Any idea what this could be?

like image 917
Christian Todd Avatar asked May 11 '26 19:05

Christian Todd


1 Answers

Debugging "is not a ..." errors

So this is likely NOT going to be the answer but this question was the top result for the error I was debugging and here is a debugging tip.

I was running mocha with the following:

test/mocha.opts

--require ts-node/register
--require source-map-support/register
--watch-extensions ts

But no matter how I imported ./app I could not get classes or functions to work despite tsc compiling fine and node and mocha working fine on the compiled .js files.

import * as app from './app'
console.log({app}); // Pretty print object

As is would happen I had a Heroku project with app.json and app.ts.

The combination of mocha and ts-node were loading the .json file extension as a higher priority instead of the .ts file and Typescript won't allow me to specify a file extension. So this behaviour is different in tsc vs mocha + ts-node.

Bonus Points - Typescript Code Coverage

Unit tests: nyc mocha src/**/*-test.ts

Integration tests: nyc mocha test/**/*.ts

package.json

{
  "nyc": {
    "extension": [
      ".ts"
    ],
    "include": [
      "src/**/*.ts"
    ],
    "exclude": [
      "src/**/*-test.ts",
      "test/**/*.ts"
    ],
    "require": [
      "ts-node/register"
    ],
    "reporter": [
      "text-summary"
    ],
    "sourceMap": true,
    "instrument": true,
    "all": true
  }
}

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "target":"es2017",

    "esModuleInterop":true,
    // "strict": true,
    "sourceMap": true,
    "outDir": "dist",
    "baseUrl": ".",
    "types":["node"],
    "rootDirs":[
      "src"    
    ]
  },
  "include": [
    "src/**/*",
    "test/**/*"
  ],
  "exclude":[
    "**/node_modules/**"
  ]

}
like image 72
Josh Peak Avatar answered May 13 '26 08:05

Josh Peak