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 usingany
you can always add it where you use them. -
moduleResolution
Since we are onNode.js
here, definitly usenode
. -
lib
The libs Typescript would use when compiling, usually determined by the target, since we useNode.js
here, there's not really any browser compatibility concerns, so theoretically you can set it toesnext
for maximum features, but it all depend on the version of youNode.js
and what you team perfer. -
module
Module style of generated Js, since we useNode
here,commonjs
is the choice -
target
Target version of generated Js. Set it to the max version if you can just likelib
-
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 namedDefinitelyTyped
. It includes all thed.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.
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
require
withimport
? Is it possible to don't touchrequire
and just add some config property intsconfig.json
or 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.js
return this error:What is your node version? If your nodejs version is old you might want to change
lib
andtarget
intsconfig.json
toes5
or 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 dev
script where the--transpileOnly
flag had to be replaced with--transpile-only
.Great post! ty!
Well you had more than one option, check this out 😄