Back

Migrate to Typescript on Node.js

Fri, Oct 04 201910 min read
Nathaniel
Recently I've migrated one of my personal projects from Javascript to Typescript.
The reason for migrating will not be covered here, since it's more of a personal choice.
This guide is for those who know something about Javascript but not much about Typescript and are mainly focus on Node.js applications.
Let's get right into it!

Add tsconfig.json

In order for Typescript to work, the first thing you need is a tsconfig.json
It tells the Typescript compiler on how to process you Typescript code and how to compile them into Javascript.
my config look like this:
{
"compilerOptions": {
"sourceMap": true,
"esModuleInterop": true,
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"lib": ["es2018"],
"module": "commonjs",
"target": "es2018",
"baseUrl": ".",
"paths": {
"*": ["node_modules/*", "src/types/*"]
},
"typeRoots": ["node_modules/@types", "src/types"],
"outDir": "./built"
},
"include": ["./src/**/*", "jest.config.js"],
"exclude": ["node_modules"]
}
now let me explain what each line means:
  • sourceMap Whether or not typescript generate sourceMap files. since sourceMap files help map the generated js file to the ts file, it's recommended to leave this on because it helps debugging.
  • esModuleInterop Support the libraries that uses commonjs style import exports by generating __importDefault and __importStar functions.
  • allowJs Allow you to use .js files in your typescript project, great for the beginning of the migration. Once it's done I'd suggest you turn this off.
  • noImplicitAny Disallow implicit use of any, this allow us to check the types more throughly. If you feel like using any you can always add it where you use them.
  • moduleResolution Since we are on Node.js here, definitly use node.
  • lib The libs Typescript would use when compiling, usually determined by the target, since we use Node.js here, there's not really any browser compatibility concerns, so theoretically you can set it to esnext for maximum features, but it all depend on the version of you Node.js and what you team perfer.
  • module Module style of generated Js, since we use Node here, commonjs is the choice
  • target Target version of generated Js. Set it to the max version if you can just like lib
  • baseUrl Base directory, . for current directory.
  • paths When importing modules, the paths to look at when matching the key. For example you can use "@types": ["src/types"] so that you do not have to type "../../../../src/types" when trying to import something deep.
  • typeRoots Directories for your type definitions, node_modules/@types is for a popular lib named DefinitelyTyped. It includes all the d.ts files that add types for most of the popular Js libraries.
  • outDir The output directory of the generated Js files.
  • include Files to include when compiling.
  • exclude Files to exclude when compiling.

Restructure the files

Typically you have a node.js project structure like this:
projectRoot
├── folder1
│ ├── file1.js
│ └── file2.js
├── folder2
│ ├── file3.js
│ └── file4.js
├── file5.js
├── config1.js
├── config2.json
└── package.json
With typescript, the structure need to be changed to something like this:
projectRoot
├── src
│ ├── folder1
│ │ └── file1.js
│ │ └── file2.js
│ ├── folder2
│ │ └── file3.js
│ │ └── file4.js
│ └── file5.js
├── config1.js
├── config2.json
├── package.json
├── tsconfig.json
└── built
The reason for this change is that typescript need a folder for generated Js and a way to determine where the typescript code are. It is especially important when you have allowJs on.
The folder names does not have to be src and built , just remember to name them correspondingly to the ones you specified in tsconfig.json.

Install the types

Now after you've done the above, time to install the Typescript and the types for you libraries.
yarn global add typescript
or
npm install -g typescript
Also for each of your third party libs:
yarn add @types/lib1 @types/lib2 --dev
or
npm install @types/lib1 @types/lib2 --save-dev

Setup the tools

ESlint

The aslant config you use for Js need to be changed now.
Here's mine:
{
"env": {
"es6": true,
"node": true
},
"extends": [
"airbnb-typescript/base",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
"plugin:jest/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }]
}
}
I use ESlint with Prettier and jest. I also use airbnb's eslint config on js and I'd like to keep using them on typescript.
You need to install the new plugins by:
yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-typescript --dev
or
npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-typescript --save-dev
Remember to change your eslint parser to @typescript-eslint/parser so that it can parse typescript.

nodemon

Nodemon is a great tool when you need to save changes and auto restart your program.
For typescript I recommend a new tool ts-node-dev. Because configuring the nodemon is a lot harder, while the ts-node-dev works right out of the box with zero configuration. They basically do the same thing anyway.
yarn add ts-node-dev ts-node --dev
or
npm install ts-node-dev ts-node --save-dev

Jest

I use jest for testing, the config need to adjust to Typescript as well
module.exports = {
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json'
}
},
moduleFileExtensions: ['ts', 'js'],
transform: {
'^.+\\.(ts)$': 'ts-jest'
},
testEnvironment: 'node'
};
Apparently you need ts-jest
yarn add ts-jest --dev
or
npm install ts-jest --save-dev
Then add ts in moduleFileExtensions, since my application is a backend only application, I didn't add jsx or tsx here, you can add them if you need to use react.
Also you need to add
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json'
}
}
to let Jest know what's you Typescript config.

Package.json scripts

The scripts section in your package.json no longer works now, you need to update them:
"scripts": {
"start": "npm run dev",
"test": "jest",
"build": "tsc",
"lint": "eslint . & echo 'lint complete'",
"dev": "ts-node-dev --respawn --transpileOnly ./src/app.ts",
"prod": "tsc && node ./built/src/app.js",
"debug": "tsc && node --inspect ./built/src/app.js"
},
The commands are mostly self explanatory, just remember to customise them according to your setup.
Then you can start your program by yarn dev or npm start later. But right now the js files haven't been changed yet.

The ignore files

Remember to add built folder in your ignore files like .gitignore and .eslintignore so that they do not generate a ton of errors.

Change the code

Now that we've setup all the things. It's time that we actually change the code itself.
Typescript was built with Javascript in mind, this means you do not have to change most of you code. But you certainly going to spend quite some time changing it.

Rename the files into .ts

Rename all your .js files into .ts , except the config files.

The imports and exports

Typescript adopts the es6 import and export syntax, this means you need to change the existing commonjs const a = require('b') and module.exports = c to import a from 'b' and exports default c
See the import and export guide on MDN to have a better understanding on how to use them.

Object property assignment

You may have code like
let a = {};
a.property1 = 'abc';
a.property2 = 123;
It's not legal in Typescript, you need to change it into something like:
let a = {
property1: 'abc',
property2: 123
}
But if you have to maintain the original structure for some reason like the property might be dynamic, then use:
let a = {} as any;
a.property1 = 'abc';
a.property2 = 123;

Add type annotations

General functions
If you have a function like this:
const f = (arg1, arg2) => {
return arg1 + arg2;
}
And they are intended only for number, then you can change it into:
const f = (arg1: number, arg2: number): number => {
return arg1 + arg2;
}
This way it cannot be used on string or any other type
Express
If you use express, then you must have some middleware function like:
(req, res, next) => {
if (req.user) {
next();
} else {
res.send('fail');
}
})
Now you need that req and res to be typed
import { Request, Response, NextFunction } from 'express';
and then change
(req: Request, res: Response, next: NextFunction) => {
if (req.user) {
next();
} else {
res.send('fail');
}
})
mongoose
Using Typescript, you want your mongoose model to have a corresponding typescript interface with it.
Suppose you have a mongoose model that goes:
import mongoose, { Schema, model } from 'mongoose';
export const exampleSchema = new Schema(
{
name: {
required: true,
type: String
},
quantity: {
type: Number
},
icon: { type: Schema.Types.ObjectId, ref: 'Image' }
},
{ timestamps: true, collection: 'Example' }
);
export default model('Example', exampleSchema);
You need add the according Typescript interface like:
export interface exampleInterface extends mongoose.Document {
name: string;
quantity: number;
icon: Schema.Types.ObjectId;
}
Also change the export into:
export default model<exampleInterface>('Example', exampleSchema);
Extend built-in Types
Sometimes you need some custom property on the built-in type, so you need to extend them.
For example, In express, you have req.user as the type Express.User, but if your user will surely different from the default one. Here's how I did it:
import { UserInterface } from '../path/to/yourOwnUserDefinition';
declare module 'express-serve-static-core' {
interface Request {
user?: UserInterface;
}
interface Response {
user?: UserInterface;
}
}
This is called Declaration Merging in Typescript. You can read the official explanation if you want to know more about it.
Note you should name the file with extension of .d.ts and put it in a separate folder and add that folder into the typeRoots in tsconfig.json for it to work globally.
Async functions
For async functions, remember to wrap you return type with Promise<> ,
Dynamic property
If your object have a dynamic property, you need something special union type annotation for it to work.
let a : string;
if (someCondition) {
a = 'name';
} else {
a = 'type';
}
const b = { name: 'something', type: 'sometype' };
const c = b[a]; // gets error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; }'.
The way to fix it:
let a: 'name' | 'type';
if (someCondition) {
a = 'name';
} else {
a = 'type';
}
const b = { name: 'something', type: 'sometype' };
const c = b[a];
Or change the last assignment into const c = b[a as 'name' | 'type'] , but apparently the first one is preferred since it checks if any unexpected value being assigned to the variable. Use this if you do not have control over the definition of the variable.

Sum up

Typescript helps a lot if you have experience in strongly typed language like C++/Java/C#, it checks many of the error at compile time. If you plan on writing an application at scale, I definitely recommend choose Typescript over Javascript.

Comments(0)

Continue with
to comment