Migrate to Typescript on Node.js
Tags: javascript, typescript, node, migrate
Published: 2019-10-04 08:11:43

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:

```json
{
  "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.

```bash
yarn global add typescript
```

or

```bash
npm install -g typescript
```

Also for each of your third party libs: 

```bash
yarn add @types/lib1 @types/lib2 --dev
```

or

```bash
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:

```json
{
  "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:

```bash
yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-typescript --dev
```

or

```bash
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.

```bash
yarn add ts-node-dev ts-node --dev
```

or

```bash
npm install ts-node-dev ts-node --save-dev
```

#### Jest

I use jest for testing, the config need to adjust to Typescript as well

```javascript
module.exports = {
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.json'
    }
  },
  moduleFileExtensions: ['ts', 'js'],
  transform: {
    '^.+\\.(ts)$': 'ts-jest'
  },
  testEnvironment: 'node'
};

```

Apparently you need `ts-jest`

```bash
yarn add ts-jest --dev
```

or

```bash
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

```javascript
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:

```json
"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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) and [export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) guide on MDN to have a better understanding on how to use them.

#### Object property assignment

You may have code like

```javascript
let a = {};
a.property1 = 'abc';
a.property2 = 123;
```

It's not legal in Typescript, you need to change it into something like:

```javascript
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:

```typescript
let a = {} as any;
a.property1 = 'abc';
a.property2 = 123;
```

#### Add type annotations

##### General functions

If you have a function like this:

```javascript
const f = (arg1, arg2) => {
	return arg1 + arg2;
}
```

And they are intended only for `number`, then you can change it into:

```typescript
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:

```javascript
(req, res, next) => {
  if (req.user) {
  	next();
  } else {
    res.send('fail');
  }
})
```

Now you need that `req` and `res` to be typed

```typescript
import { Request, Response, NextFunction } from 'express';
```

and then change

```typescript
(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:

```javascript
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:

```typescript
export interface exampleInterface extends mongoose.Document {
  name: string;
  quantity: number;
  icon: Schema.Types.ObjectId;
}
```

Also change the export into:

```typescript
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:

```typescript
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](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) 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.

```typescript
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:

```typescript
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.