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:
- 
sourceMapWhether 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. - 
esModuleInteropSupport the libraries that uses commonjs style import exports by generating__importDefaultand__importStarfunctions. - 
allowJsAllow you to use.jsfiles in your typescript project, great for the beginning of the migration. Once it's done I'd suggest you turn this off. - 
noImplicitAnyDisallow implicit use of any, this allow us to check the types more throughly. If you feel like usinganyyou can always add it where you use them. - 
moduleResolutionSince we are onNode.jshere, definitly usenode. - 
libThe libs Typescript would use when compiling, usually determined by the target, since we useNode.jshere, there's not really any browser compatibility concerns, so theoretically you can set it toesnextfor maximum features, but it all depend on the version of youNode.jsand what you team perfer. - 
moduleModule style of generated Js, since we useNodehere,commonjsis the choice - 
targetTarget version of generated Js. Set it to the max version if you can just likelib - 
baseUrlBase directory,.for current directory. - 
pathsWhen 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. - 
typeRootsDirectories for your type definitions,node_modules/@typesis for a popular lib namedDefinitelyTyped. It includes all thed.tsfiles that add types for most of the popular Js libraries. - 
outDirThe output directory of the generated Js files. - 
includeFiles to include when compiling. - 
excludeFiles 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.
              
    
Top comments (9)
Thanks for the great article!
I have a relatively big Node project written in JS which I want to convert to TS. Is it necessary to replace all
requirewithimport? Is it possible to don't touchrequireand just add some config property intsconfig.jsonor something?You can find and replace your
const module = require("module")withimport module = require("module"), which also works with typescript.Also there is a vs-code plugin to help you:
marketplace.visualstudio.com/items...
Thanks!
Very good article.
after i run
node .\built\app.jsreturn this error:What is your node version? If your nodejs version is old you might want to change
libandtargetintsconfig.jsontoes5or olderI solved the issue by removing
"type": "module"frompackage.json.Thanks for the great article!
There was only one issue I've found in the
npm run devscript where the--transpileOnlyflag had to be replaced with--transpile-only.Well you had more than one option, check this out π
Great post! ty!