Skip to content

50.003 - Express.js (JS to TS Migration)

Learning Outcomes

By the end of this unit, you should be able to

  1. Migrate an express.js project from JavaScript to TypeScript

JavaScript to TypeScript Migration

The end goal is to convert all JS codes into TS.

Here are the steps:

Step 1: Install the TypeScript Dependencies.

In the project root folder

npm install --save-dev typescript ts-node ts-node-dev @types/node @types/express

Step 2: Configure tsconfig.json for Express

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node", 
    "sourceMap": true // set it to false for production deployment
  },
  "include": ["**/*.ts"], // compile all ts files found in the root folders and sub-folders.
  "exclude": ["node_modules", "dist", "public"]
}                                               

In the above, there are few key attribtues we should pay attention

  • "target": "ES2020" sets the target JS to be ESMAScript 2020 standard,
  • "module": "commonjs" indicates that the project following the commonjs style module structure.
  • "outDir" sets where the output JS codes will be stored. - "strict": true ensures the strictest type checking is enforced.
  • "forceConsistentCasingInFileNames": true enforce filenames are case-sensitive even the OS is not, e.g. Windows.
  • "sourceMap": true enable a source mapping between TS and JS files, for the ease of debugging and testing, which should be turned off for production deployment.

Step 3: Update package.json scripts section

We update the scripts field as follows

1
2
3
4
5
6
7
  ...
  "scripts": {
    "build": "tsc && node -e \"fs.cpSync('./public', './dist/public/', { recursive: true })\"",
    "start": "node ./dist/bin/www",
    "dev": "tsc && node -e \"fs.cpSync('./public', './dist/public/', { recursive: true })\" && node ./dist/bin/www"
  },
  ...

when npm run build is executed, we call the ts compiler and also copy the public folder's content into dist/public which capture the client side javascript and CSS. npm run start will trigger node to run ./dist/bin/www.

Alternatively, we could use ts-node to run the .ts file directly.

1
2
3
  "scripts": {
    "start": "ts-node ./bin/www.ts"
  }

This is only used as quick setup for development only, not recommended for production project.

Step 4: Convert the JS files

Starting from app.js, routes/*.js then models/*.js

Step 4.1: Convert the module imports

JS imports

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var process = require('process');
var db = require('./models/db.js');
var staffModel = require('./models/staff.js');
var deptModel = require('./models/dept.js');
var workfModel = require('./models/work.js');

should be converted into TS imports

import createError from 'http-errors';
import express, { Request, Response, NextFunction, Router, Application } from 'express';
import path from 'path';
import cookieParser from 'cookie-parser';
import logger from 'morgan';
import proc from 'process';
import * as db from './models/db';
import * as staffModel from './models/staff';
import * as deptModel from './models/dept';
import * as workfModel from './models/work';

Apply this update to all server side JS files (excluding those in public)

Step 4.2 add type annotations to function arguments and return types

For example,

1
2
3
4
5
router.get('/all/', async function(req, res, next) {
    const depts = await deptModel.all();
    res.set('Access-Control-Allow-Origin', 'http://localhost:5000');
    res.send(`${JSON.stringify(depts)}`);
});

should be update to

1
2
3
4
5
router.get('/all/', async function(req: Request, res: Response, next: (err: any) => void) {
  const depts = await deptModel.all();
  res.set('Access-Control-Allow-Origin', 'http://localhost:5000');
  res.send(`${JSON.stringify(depts)}`);
});

Apply this update to all server side JS files (excluding those in public)

Step 4.3: Add type annotations the class attributes and method definition (in the models)

class Staff {
    constructor(id, name, code) {
        this.id = id;
        this.name = name;
        this.code = code;
    }

    static newStaff(name, code) {
        // in JS, only one constructor is allowed
        // we need a factory method
        return new Staff(undefined, name, code);
    }
}

should be updated to

export class Staff {
  id: number | undefined;
  name: string;
  code: string | undefined;

  constructor(id: number | undefined, name: string, code: string | undefined) {
    this.id = id;
    this.name = name;
    this.code = code;
  }

  static newStaff(name: string, code: string): Staff {
    return new Staff(undefined, name, code);
  }
}

Apply this update to all server side JS files (excluding those in public)

Step 4.4 Fixing the export

Instead of declaring the export at the end of the file.

module.exports =  { Staff, all, findOneById, findByDept, findOneByName, sync, insertOne, insertMany, deleteOne }

add export keyboard to every function and class that should be visible to other modules.

For example the export class Staff in the step 4.4.

References

  1. https://medium.com/@chiragmehta900/migrate-your-node-js-express-app-to-typescript-step-by-step-2026-15e4ff048e1f