Author: Chen Xiaoqiang

preface

If you have a Node.js project and want to remodel with TypeScript, this article might be helpful. As TypeScript grows in popularity, this article doesn’t cover the reasons for using TypeScript, nor does it cover basic concepts. This article explains how to remodel an old Node.js project using TypeScript, including directory structing, typescript-esLint configuration, tsconfig configuration, debugging, common error handling, and more. Due to the limited space available, the node.js project can integrate a wide variety of technologies, so forgive the scenarios that are not covered.

Step 1 Adjust the directory structure

Node.js programs, due to fast support for new syntax (async/await from V7.6.0 on), do not need to use Babel, Webapck or other compilation tools for most scenarios, and therefore have very few dist directories for compiled files, whereas TypeScript does. So the key is to separate out a source directory and compile the target directory, recommended directory structure is as follows, in addition, according to the different technology stacks and a bunch of other configuration files such as prettier, Travis, etc is omitted here.

|-- assets            # Store project images, videos and other resource files
|-- bin               # require('.. /dist/cli'), and note the #! /usr/bin/env node
|-- dist              Dist is the directory of compiled files. Note that the main field in package.json should point to the dist directory
|-- docs              # Store project related documents
|-- scripts           The scripts field in package.json is the script file to execute
|-- src               * * * * * * * * * * * * * * * * * * * * * * * * * *
    |-- sub           # subdirectories
    |-- cli.ts        # CLI entry file
    |-- index.ts      # API entry file
|-- templates         # Store files such as JSON and template
|-- tests             # Test the file directory
|-- typings           # store the ts declaration file, which is used to supplement third-party packages that do not have ts declaration
|-- .eslintignore     # eslint ignores rule configurations
|-- .eslintrc.js      # ESLint Rule configuration
|-- .gitignore        # git ignores rules
|-- package.json      # 
|-- README.md         # Project Description
|-- tsconfig.json     # typescript configuration, do not modify
Copy the code

Install and configure TypeScript

After the directory structure is adjusted, execute in your project root directory

  1. npm i typescript -DInstall typescript and save it to the Dev dependency
  2. node ./node_modules/.bin/tsc --initTo initialize the TypeScript project and generate a tsconfig.json configuration file

If you chose global install in step 1, you can use TSC –init directly in Step 2

After the initialization command is executed, a default configuration file is generated. For more detailed configuration and description, you can refer to the official document. Here is a basic recommended configuration according to the previous project structure, and some configurations will be explained later.

{"compilerOptions": {// "incremental": true, /* Incremental compilation speeds up compilation */ "target": "ES2019", /* Compile target ES version */ "module": "Commonjs ", /* compile the target module system */ // "lib": [], /* List of libraries that need to be imported during compilation */ "declaration": true, /* Create declaration at compile time */ "outDir": "Dist", compiled output directory * / / * ts "rootDir" : "SRC", compile the root directory / * ts. * / / / "importHelpers" : True, /* Import helper functions (such as __importDefault) from tslib */ "strict": True */ "noUnusedLocals": /* Strict mode = noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, etc. */ "noUnusedParameters": true, /* "noUnusedParameters": true, /* "noImplicitReturns": True, / * have a code path has no return value times wrong * / "noFallthroughCasesInSwitch" : true, / * are not allowed to switch the case statement through * / "moduleResolution" : "Node ", /* module resolution policy */ "typeRoots": [/ * to include the type of list * / declaration file path ". / typings ", ". / node_modules / @ types "], "allowSyntheticDefaultImports" : */ "esModuleInterop": */ "esModuleInterop": False /* allow to inject utility classes (__importDefault, __importStar) into the code when compiling the generated file for compatibility with ESM and CommonJS */}, "include": [/ * need to compile the file * / SRC / / *. * * ts "and" typings / / *. * * ts "], "exclude" : [/ * compile need to rule out file * / node_modules/" * * "],}Copy the code

Step three, source file adjustment

Change all.js files to.ts files

This step is relatively simple. You can use tools such as gulp to change the suffix of all files to TS and extract them into the SRC directory according to your project.

Template file extraction

Since TypeScript can only process ts, TSX, JS, and JSX files during compilation, you can extract some templates such as JSON and HTML files from the templates directory without compilation.

Add scripts to package.json

/node_modules/.bin/ TSC; / /node_modules/.bin/ TSC; But it may not match the typescript version your project depends on.) add the following script to package.json. You can then compile it directly from NPM Run build or NPM Run Watch.

{
  "scripts": {"build":"tsc"."watch":"tsc --watch"}}Copy the code

Step four: TypeScript code specification

If your IDE is VSCode, but TypeScript and VSCode are both Microsoft’s sons, you should use VSCode with TypeScript. After this step, you will see a lot of red warnings in ts files. Something like this:

TypeScript-ESLint

Earlier TypeScript projects typically used TSLint, but in early 2019 TypeScript officially decided to adopt ESLint entirely. So the TypeScript specification, which uses ESLint directly, installs dependencies first: NPM I eslint @typescript-eslint/ Parser @typescript-eslint/eslint-plugin -d Then create an.eslintrc.js file in the root directory. The simplest configuration is as follows

module.exports = {
  'parser':'@typescript-eslint/parser'.// Use @typescript-eslint/parser to parse TS files
  'extends': ['plugin:@typescript-eslint/recommended'].// Let ESLint inherit the rules defined by @typescript-eslint/recommended
  'env': {'node': true}}Copy the code

Because the @typescript-eslint/recommended rules are not perfect, you need to complement esLint’s rules, such as the prohibition of no-multi-spaces. You can use Standard to install dependencies.

If your project is already using ESLint and has its own specification, you can simply adjust the.eslintrc.js configuration without installing dependencies

npm i eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard -D For the above packages, eslint-config-standard is the rule set, and the following are its dependencies. Eslintrc.js configuration:

module.exports = {
  'parser':'@typescript-eslint/parser'.'extends': ['standard'.'plugin:@typescript-eslint/recommended'].// Extends extends with the standard specification
  'env': {'node': true}}Copy the code

VSCode integrates ESLint configuration

For development convenience we can integrate ESLint configurations into VSCode, both for real-time prompts and auto-fix on save.

  1. Install the ESLint plug-in for VSCode
  2. Modify the ESLint plugin configuration: Set => Extend => ESLint => Check (Auto Fix On Save) => Edit in settings.json, as shown in figure:
  3. Since ESLint only validates.js files by default, you need to add ESLint configuration to settings.json:
{
    "eslint.enable": true.// Whether to enable VScode esLint
    "eslint.autoFixOnSave": true.// Whether to fix automatically when saving
    "eslint.options": {    // Specifies the suffix for the files that vsCode's ESLint will handle
        "extensions": [
            ".js".// ".vue",
            ".ts".".tsx"]},"eslint.validate": [     // Determine the calibration criteria
        "javascript"."javascriptreact"./ / {
        // "language": "html",
        // "autoFix": true
        // },
        / / {
        // "language": "vue",
        // "autoFix": true
        // },
        {
            "language": "typescript"."autoFix": true
        },
        {
            "language": "typescriptreact"."autoFix": true}}]Copy the code
  1. If VSCode fails to prompt you, try restarting the ESLint plugin, moving the project out of the workspace, and then adding it back in.

Step 5. Resolve the error

This step content is a bit much, can be detailed. Note that some parts of the following solution report use “any method” (not recommended), this is to make the project run as soon as possible, after all, it is an old project transformation, can not be in place in one step.

Module not found

The Node.js project is the CommonJS specification and exports a module using require: const path = require(‘path’); The first thing you see is an error at require:

Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i @types/node`.ts(2580)
Copy the code

At this point you might think to change the TypeScript import to import * as path from ‘path’, and you’ll see an error at path:

Module 'path' could not be found. ts(2307)Copy the code

The path module and require are both node.js components. You need to install the node.js declaration file npm@types/Node -d.

TypeScript import issues

After installing the Node declaration documents, written before: const path = the require (” path “) is still at the require complains, but this is not a TypeScript error, but ESLint error:

Require statement not part of import statement .eslint(@typescript-eslint/no-var-requires)
Copy the code

This import is not recommended because it exports any objects with no type support. This is why ESLint should be configured first.

Next, let’s change the module import to TypeScript import. There are four ways to write the module import.

import * as mod from ‘mod’

For the CommonJS module, using this notation, let’s look at the difference between before and after compilation. Note that we are modifying the Node.js project, so we configure “module”: “commonjs” in tsConfig. Test. Ts file:

import * as path from 'path'
console.log(path);
Copy the code

The compiled test.js file:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
console.log(path);
Copy the code

As you can see, TypeScript adds __esModule:true to the compiled module, identifying it as an ES6 module. If you configure “esModuleInterop”:true in tsconfig, the compiled test.js file looks like this:

"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if(mod ! =null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = __importStar(require("path"));
console.log(path);
Copy the code

}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} Otherwise return module.exports.defalut = module.exports(with circular references removed). If you don’t want to inject such a utility function into every file you compile, you can configure “importHelpers”:true and compile the test.js file as follows:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const path = tslib_1.__importStar(require("path"));
console.log(path);
Copy the code

Careful students may notice that the “esModuleInterop”:true configuration adds __importStar that does nothing but add complexity to the require in the above scenarios. Is it possible to remove this configuration? Let’s move on.

If you use the import import other source files within the program, because the original commonjs wording, it will prompt you file “/ path/to/project/SRC/mod. The ts” is not a module. Ts (2306). In this case, change the imported module to ES6 export

import { fun } from ‘mod’

Modify the test.ts file. “esModuleInterop”:true is still configured

import { resolve } from 'path'
console.log(resolve)
Copy the code

The compiled test.js file

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = require("path");
console.log(path_1.resolve);
Copy the code

You can see that when you export a single property, you don’t add a utility class, but you change the single property export to an entire module export and change the original function call expression to a member function call expression.

import mod from ‘mod’

This syntax is the export default, so be careful.

Modify the test.ts file as usual and configure “esModuleInterop”:true and “importHelpers”:false for display purposes.

import path from 'path'
console.log(path)
Copy the code

The compiled test.js file:

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = __importDefault(require("path"));
console.log(path_1.default);

Copy the code

}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} Otherwise, {default:module.exports} is returned. This is a compatibility for modules that do not have a default export. The FS module is commonjs, does not have an __esModule attribute, and exports using modules.exports. Path_1 in the above code is actually {default:module.exports}, so path_1.default refers to the original path module, so you can see that the conversion is normal.

There is a pitfall to this approach. For example, if you have a third party module whose files have been converted with Babel orts, the module code will probably contain the __esModule attribute, but there is no export.default export. It will appear that mod.default refers to undefined. Even worse, neither the IDE nor the compiler reported any errors. If this basic type checking doesn’t work, why do I need TypeScript?

Fortunately, tsconfig provides a configuration allowSyntheticDefaultImports, allow never set the default default import export modules, it is important to note that this property does not have any influence on code generation, just silently. Also, in configuring “module”: “Commonjs”, its value is and esModuleInterop synchronous, which means we set up in front of the “esModuleInterop” : true, equal to at the same time set up a “allowSyntheticDefaultImports” : true. This allows for no hint.

After manually modify “allowSyntheticDefaultImports” : false, will find that ts file import path from ‘path’ place prompted module “” path” “there is no default exported. Ts (1192), with this prompt, we change it to import * as path from path, which can effectively avoid the above trap.

import mod = require(‘mod’);

This writing is a bit odd, at first glance, half ES6 module writing and half CommonJS writing. This is an earlier declaration file that is exported using the export = mod syntax. So if you encounter such a declaration file, use this notation. Moment = require(‘moment’) const moment = require(‘moment’); moment(); When you change import * as moment from ‘moment’, moment(); Statement prompt:

This expression is not callable.
  Type 'typeof moment' has no call signatures.ts(2349)
gulp-task.ts(15, 1): Type originates at this import. A namespace-style import cannot be called or constructed, and will cause a failure at runtime. Consider using a default import or import require here instead.
Copy the code

Prompt you to use default import or import require, when you change to default import: import moment from’moment’; moment(); , the import statement will prompt:

Module '"/path/to/project/src/moment"' can only be default-imported using the 'esModuleInterop' flagts(1259)
moment.d.ts(736, 1): This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.
Copy the code

Import moment = require(‘moment’);

The new TS module (Declare Module ‘mod’), like the path module, also supports Import assignment, but it is not recommended to do so.

The import summary

“EsModuleInterop “:true “esModuleInterop”:true “esModuleInterop”:true “esModuleInterop”:true “esModuleInterop”:true But considering the scene, some import ES6 module may need to retain, will no longer be discussed here, it’s important to note that the manual configuration “allowSyntheticDefaultImports” : false to avoid the trap. Fixing the import problem solves most of the problem, ensuring that your compiled files do not import modules with undefined.

The declaration file was not found

There is no ts declaration file in some third-party packages.

The declaration file for the module "mod" could not be found. "/ path/to/project/SRC/index, js" implicit"any"Type. Try `npm install @types/mod`if it exists or add a new declaration (.d.ts) file containing `declare module 'mod'; `ts(7016)Copy the code

Install the corresponding package as prompted, add -d to the dev dependency, and install the corresponding version. For example, if you have the gulp@3 version installed, do not install @types/gulp for gulp@4

In rare cases, the third-party package does not have a declaration file in it, and neither does the corresponding @types/mod package. In this case, you have to add a declaration file to the third-party package to solve the problem. We add the declaration file to the Typings folder, using the package name as a subdirectory name. The simplest way to write it is as follows, so that the IDE and TypeScript compilation will not report errors.

declare module 'mod'
Copy the code

As for why you need to put it in the typings directory and use the package name as the subpackage directory, it is not recognized by TS-Node (which will be mentioned below). Let’s follow the specification of TS-Node for now.

The Class constructor this.xx failed to initialize

It is common practice to initialize this in Class constructors, but in TS, you have to define it first. All this properties must be declared first, something like this:

class Person {
  name: string;
  constructor (name:string) {
    this.name = name; }}Copy the code

Of course, if you have a lot of code and it takes a lot of time, use the ‘any ‘method and just use any for each property.

Object attribute assignment error reported

Dynamic objects are characteristic of JS, I first define an object, whenever I can directly add properties to the object, this error, the fastest way to change the object is to declare any type. Again, the correct posture is to declare Interface or Type, not any, which is used only to quickly remodel the old project so that it can run first.

let obj:any = {};
obj.name = 'string'
Copy the code

The parameter “arg” has an implicit “any” type

const init = (opt: any) = > {
  console.log(opt)
}
Copy the code

Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type) This means that the method should have a return value, which is just a warning, but does not affect the operation of the project. Ignore it first and improve it later.

Unused function arguments

const result = code.replace(/version=(1)/g.function (_a: string, b: number) :string {
  return `version=${++b}`
})
Copy the code

Some callback parameters may not be used. Change the parameter name to _ or start with _.

Use this in the function

According to the different writing method, there may be four kinds of error:

  1. Attribute name does not exist on type NodeModule. ts(2339)
  2. Attribute 'name' does not exist on type 'typeof globalThis' ts(2339)
  3. "This" implicitly has type "any" because it has no type annotation. ts(2683)
  4. The containing arrow function captures the global value of 'this'.ts(7041)

This is done by taking this as the first argument to the function, which is automatically removed after compilation.

export default function (this:any,one:'string') {
  this.name = 'haha';
}
Copy the code

Step 6. Debug the configuration

After these steps, your project is ready to run. There are a lot of warnings and any, but you are there. Now it is time to solve the debugging problem.

Method 1. Debug the generated dist file

VSCode reference configuration (/ path/to/project /. VSCode/launch json) as follows

{
  "configurations": [{
    "type": "node"."request": "launch"."name": "debug"."program": "/path/to/wxa-cli/dist/cli.js"."args": [
        "xx"]]}}Copy the code

Method 2: Debug the TS file directly

Use TS-Node for line debugging. VSCode reference configuration is as follows. For details, see TS-Node

{
  "configurations": [{
    "type": "node"."request": "launch"."name": "debug"."runtimeArgs": [
      "-r"."ts-node/register"]."args": [
      "${workspaceFolder}/src/cli.ts"."xx"]]}}Copy the code

Step 7. Strengthen and eliminate any

The next step is to add interfaces, types, and gradually eliminate any in the code, which has been sprayed to the ground by the industry, but don’t try to remove any. Js is still a dynamic language, and TypeScript is a superset of static languages. But it’s still a long way from reaching the level of a pure static language like Java.

This is the end of the article, only involves the tool class node.js project transformation, the scene is limited, can not represent all Node.js projects, I hope it can be helpful to you.


If you think this post is valuable to you, please like it and follow us on our website and our WecTeam account, where we will post quality articles every week: