50.003 - Express.js (JS to TS Migration)
Learning Outcomes
By the end of this unit, you should be able to
- 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
|
| {
"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
| ...
"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.
| "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,
| 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
| 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
https://medium.com/@chiragmehta900/migrate-your-node-js-express-app-to-typescript-step-by-step-2026-15e4ff048e1f