Node.js 项目 TypeScript 改造指南

Stella981
• 阅读 1544

前言

如果你有一个 Node.js 项目,并想使用 TypeScript 进行改造,那本文对你或许会有帮助。

TypeScript 越来越火,本文不讲为什么要使用 TypeScript,也不讲基本概念。本文讲的是如何将一个旧的 Node.js 项目使用 TypeScript 进行改造,包括目录结构调整、TypeScript-ESLint 配置、tsconfig 配置、调试、常见错误处理等。

由于篇幅有限,Node.js 项目能集成的技术也是五花八门,未覆盖到的场景还请见谅。

步骤一、调整目录结构

Node.js 程序,由于对新语法的支持比较快(如async/await从v7.6.0开始支持),大部分场景是不需要用到 babel、webapck 等编译工具的,因此也很少有编译文件的dist目录,而 TypeScript 是需要编译的,所以重点是要独立出一个源码目录编译目标目录,推荐的目录结构如下,另外,根据不同技术栈还有一堆其他的配置文件如 prettier、travis 等等这里就省略了。

|-- assets            # 存放项目的图片、视频等资源文件

步骤二、TypeScript安装与配置

目录结构调整后,在你的项目根目录执行:

(1)npm i typescript -D,安装 typescript,保存到 dev 依赖

(2)node ./node_modules/.bin/tsc --init,初始化 TypeScript 项目,生成一个 tsconfig.json 配置文件

备注:如果第1步选择全局安装,那第2步中可以直接使用tsc --init

执行初始化命令后会生成一份默认配置文件,更详细的配置及说明可以自行查阅官方文档,这里根据前面的项目结构贴出一份基本的推荐配置,部分配置下文会解释。

{

步骤三、源码文件调整

将所有.js文件改为.ts文件

这一步比较简单,可以根据自身项目情况,借助 gulp 等工具将所有文件后缀改成ts并提取到src目录。

模板文件提取

由于 TypeScript 在编译时只能处理 ts、tsx、js、jsx 这几类文件,因此项目中如果用到了一些模板如 json、html 等文件,这些是不需要编译的,可以提取到 templates 目录。

packaeg.json中添加scripts

前面我们将 typescript 包安装到项目依赖后,避免每次执行编译时都需要输入node ./node_modules/.bin/tsc(全局安装忽略,不建议这么做,其他同学可能已经全局安装了,但可能会与你项目所依赖的 typescript 版本不一致),在 package.json 中添加以下脚本。后续就可以直接通过npm run build或者npm run watch来编译了。

{

步骤四、TypeScript代码规范

假设你用的 IDE 是 VSCode,TypeScript 与 VSCode 都是微软亲儿子,用 TypeScript 你就老老实实用 VSCode 吧,上述步骤以后,ts 文件中会出现大量飘红警告。类似这样:

Node.js 项目 TypeScript 改造指南

报错

先不要着急去解决错误,因为还需要对 TypeScript 添加 ESLint 配置,避免改多遍,先把 ESLint 配置好,当然,你如果喜欢 Pretitter,可以把它加上,本文就不介绍如何集成 Pretitter 了。

TypeScript-ESLint

早期的 TypeScript 项目一般使用 TSLint ,但2019年初 TypeScript 官方决定全面采用 ESLint,因此 TypeScript 的规范,直接使用 ESLint 就好,首先安装依赖: npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

接着在根目录下新建.eslintrc.js文件,最简单的配置如下:

module.exports = {

由于 @typescript-eslint/recommended 的规则并不完善,因此还需要补充ESLint的规则,如禁止使用多个空格(no-multi-spaces)等。可以使用standard[1],安装依赖。

如果你项目已经在使用 ESLint,并有自己的规范,则不用再安装依赖,直接调整 .eslintrc.js 配置即可。

npm i eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard -D 以上几个包,eslint-config-standard 是规则集,后面几个都是它的依赖。接来下调整. eslintrc.js 配置:

module.exports = {

VSCode中集成ESLint配置

为了开发方便我们可以在 VSCode 中集成 ESLint 的配置,一是用于实时提示,二是可以在保存时自动 fix。

Node.js 项目 TypeScript 改造指南

vscode-demo

1.安装 VSCode 的 ESLint 插件。2.修改 ESLint 插件配置:设置 => 扩展 => ESLint => 打钩(Auto Fix On Save) => 在 settings.json 中编辑,如图:

Node.js 项目 TypeScript 改造指南

VSCode配置ESLint

1.由于 ESLint 默认只校验 .js 文件,因此需要在在 settings.json 中添加 ESLint 相关配置:

{

1.若遇到 VSCode 无法提示,可尝试重启下 ESLint 插件、将项目移出工作区再重新加回来。

步骤五、解决报错

这个步骤内容有点多,可以细品一下。注意,下述解决报错有些地方用了“any大法”(不推荐),这是为了能让项目尽快 run 起来,毕竟是旧项目改造,不可能一步到位。

找不到模块

Node.js 项目是 commonjs 规范,使用 require 导出一个模块:const path = require('path');首先看到的是 require 处的错误:

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

此时你可能会想到改成 TypeScript 的 import 写法:import * as path from 'path',接着你会看到在 path 处的错误:

找不到模块“path”。ts(2307)

这两个是同一个问题,path 模块和 require 都是 Node.js 的东西,需要安装 Node.js 的声明文件,npm i @types/node -D

TypeScript的import问题

安装完 Node 的声明文件后,之前的写法:const path = require('path')在 require 处仍然会报错,不过这次不是 TypeScript 报错,而是 ESLint 报错:

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

意思是不推荐这种导入写法,因为这种 commonjs 写法导出来的对象是 any,没有类型支持。这也是为啥前面说不用着急改,先做好 ESLint 配置。

接着我们将模块导入改成 TypeScript 的 import,这里共有4种写法,分别讲一下需要注意的问题。

import * as mod from 'mod'

针对 commonjs 模块,使用此写法,我们来看看编译前后的区别,注意我们改造的是 Node.js 项目,因此我们 tsconfig 中配置"module": "commonjs"

test.ts 文件:

import * as path from 'path'

编译后的 test.js 文件:

"use strict";

可以看到,TypeScript 对编译后给模块加上了__esModule:true,标识这是一个 ES6 模块,如果你在 tsconfig 中配置"esModuleInterop":true,编译后的 test.js 文件如下:

可以看到针对 import * 写法,在编译成 commonjs 后包裹了一个__importStar工具函数,其作用是:如果导入模块 __esModule 属性为 true,则直接返回 module.exports。否则返回module.exports.defalut = module.exports(消除了循环引用)。如果你不想在编译后的每个文件中都注入这么一段工具函数,可以配置"importHelpers":true,编译后的 test.js 文件如下:

"use strict";

细心的同学可能会发现,"esModuleInterop":true这个配置添加的__importStar在以上场景除了增加 require 复杂度,没什么其他作用。那是否可以去掉这个配置呢,我们接着往下看。

如果你用 import 导入的项目内的其他源文件,由于原先 commonjs 写法,会提示你文件“/path/to/project/src/mod.ts”不是模块。ts(2306),此时,需要将被导入的模块修改为 ES6 的 export 写法

import { fun } from 'mod'

修改 test.ts 文件,依然是配置了:"esModuleInterop":true

import { resolve } from 'path'

编译后的 test.js 文件

"use strict";

可以看出导出单个属性时,并不会添加工具类,但会将单个属性导出修改为整个模块导出,并将原来的函数调用表达式修改为成员函数调用表达式。

import mod from 'mod'

这个语法是导出默认值,要特别注意。

照例修改 test.ts 文件,配置"esModuleInterop":true,为了方便展示,配置"importHelpers":false

import path from 'path'

编译后的 test.js 文件:

"use strict";

可以看到针对 import mod 这种写法,在编译成 commonjs 后包裹了一个__importDefault工具函数,其作用是:如果导入模块__esModule为 true,则直接返回module.exports。否则返回{default:module.exports}。这个是针对没有默认导出的模块的一种兼容,fs 模块是 commonjs,并没有__esModule属性,使用modules.exports导出。上述代码中的path_1实际是{default:module.exports},因此path_1.default指向的是原 path 模块,可以看出转换是正常的。

但这种方式是有个陷阱,举个例子,如果有第三方模块,其文件是用 babel 或者也是 ts 转换过的,那其模块代码很有可能包含了 __esModule 属性,但同时没有exports.default导出,此时就会出现 mod.default 指向的是undefined更要命的是,IDE和编译器没有任何报错。如果这个最基本的类型检查都解决不了,那我要 TypeScript 何用?

所幸,tsconfig 提供了一个配置allowSyntheticDefaultImports,意思是允许从没有设置默认导出的模块中默认导入,需要注意的是,这个属性并不会对代码的生成有任何影响,仅仅是给出提示。另外,在配置"module": "commonjs"时,其值是和esModuleInterop同步的,也就是说我们前面设置了"esModuleInterop":true,相当于同时设置了"allowSyntheticDefaultImports":true。这个允许也就是不会提示。

手动修改"allowSyntheticDefaultImports":false后,会发现 ts 文件中import path from 'path'处出现提示模块“"path"”没有默认导出。ts(1192),通过这个提示,我们将其修改为import * as path from path,可以有效避免上述陷阱

import mod = require('mod');

这种写法有点奇怪,乍一看,一半的 ES6 模块写法和一半的 commonjs 写法。其实这是针对早期的声明文件,使用了export = mod语法进行导出。因此如果碰上这种声明文件,就使用此种写法。拿第三方包 moment 举例:你原来的写法是const moment = require('moment'); moment(); 当你改成import * as moment from 'moment'时,moment();语句处会提示:

This expression is not callable.

提示你使用default导入或import require写法,当你改成default导入时:import moment from'moment'; moment(); ,则在导入语句处会提示:

Module '"/path/to/project/src/moment"' can only be default-imported using the 'esModuleInterop' flagts(1259)

改成import moment = require('moment'),则没有任何报错,对应的类型检测也都正常。

新的 ts 声明文件写法(declare module 'mod'),如前面所说的path模块,也支持此种 Import assignment 写法,但建议还是不要这样写了。

import小结

看完后再来回顾前面的问题:是否可以去掉这个配置"esModuleInterop":true 个人认为在 Node.js 场景是可以去掉的我并不想看到那两个多余的工具函数。但考虑到一些导入 ES6 模块的场景,可能需要保留,这里就不再讨论了,需要注意的是手动配置"allowSyntheticDefaultImports":false避免陷阱。解决了 import 问题,其实问题就解决一大半了,确保了你编译后的文件引入的模块不会出现 undefined。

找不到声明文件

部分第三方包,其包内没有 ts 声明文件,此时报错如下:

无法找到模块“mod”的声明文件。“/path/to/project/src/index.js”隐式拥有 "any" 类型。

根据提示安装对应包即可,注意添加 -D 保存到 dev 依赖,注意安装对应版本。比如你安装了 gulp@3 的版本,就不要安装 gulp@4 的 @types/gulp

极少情况,第三方包内既没有声明文件,对应的@types/mod包也没有,此时为了解决报错,只能自己给第三方包添加声明文件了。我们将声明文件补充到typings文件夹中,以包名作为子目录名,最简单的写法如下,这样 IDE 和 TypeScript 编译便不会报错了。

declare module 'mod'

至于为什么需要放在 typings 目录,并且以包名作为子包目录,因为不这样写,ts-node(下文会提到)识别不了,暂且按照 ts-node 的规范来吧。

Class构造函数this.xx初始化报错

在 Class 的构造函数中对 this 属性进行初始化是常见做法,但在 ts 中,你得先定义。所有 this 属性,都要先声明,类似这样:

class Person {

当然,如果你代码比较多,改造太耗时间,那就用'any大法'吧,每一个属性直接用 any 就完事了。

对象属性赋值报错

动态对象是 js 的特色,我先定义个对象,不管啥时候我都可以直接往里面加属性,这种报错,最快的改造办法就是给对象申明 any 类型。再次申明,正确的姿势是申明 Interface 或者 Type,而不是 any,此处用 any 只是为了快速改造旧项目让其能先 run 起来。

let obj:any = {};

参数“arg”隐式具有“any”类型

const init = (opt: any) => {

除了参数隐式 any 外,此处还会有警告Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type),意思是方法需要有返回值,只是警告,不影响项目运行,先忽略,后续再完善。

未使用的函数参数

const result = code.replace(/version=(1)/g, function (_a: string, b: number): string {

有些回调函数参数可能是用不上的,将参数名字改成_或者_开头

函数中使用this

根据写法不同,大概会有以下4种报错:

1.类型“NodeModule”上不存在属性“name”。ts(2339)2.类型“typeof globalThis”上不存在属性“name”。ts(2339)3."this" 隐式具有类型 "any",因为它没有类型注释。ts(2683)4.The containing arrow function captures the global value of 'this'.ts(7041)

处理方式是将 this 作为函数参数,并作为第一个参数,编译后会自动去掉第一个 this 参数。

export default function (this:any,one:'string') {

步骤六、调试配置

经过以上步骤,你的项目就能 run 起来了,虽然有很多警告和 any,但好歹已经算是走过来了,接下来就是解决调试问题。

方法一、调试生成后的dist文件

VSCode 参考配置(/path/to/project/.vscode/launch.json)如下:

{

Node.js 项目 TypeScript 改造指南

VSCode调试js

方法二、直接调试ts文件

使用 ts-node进 行调试,VSCode 参考配置如下,详见ts-node[2]

{

Node.js 项目 TypeScript 改造指南

VSCode调试ts

步骤七、类型加强、消除any

接下来要做的就是补充 Interface、Type,逐步将代码中的被业界喷得体无完肤的 any 干掉,但不要妄想去掉所有 any ,js 语言说到底还是动态语言,TypeScript 虽然是其超集往静态语言靠,但要做到 Java 这种纯静态语言程度还是有一段距离的。

到这就算结束了,文中只涉及到了工具类的 Node.js 项目改造,场景有限,并不能代表所有 Node.js 项目,希望能对大家有所帮助。

References

[1] standard: https://standardjs.com/readme-zhcn.html
[2] ts-node: https://github.com/TypeStrong/ts-node#visual-studio-code

往期精彩回顾

入门 Node.js Net 模块构建 TCP 网络服务

Node.js 中实践 Redis Lua 脚本

Node.js 核心模块都在使用的  Events 模块你了解吗?

Node.js 中实践基于 Redis 的分布式锁实现

分享 10 道 Nodejs EventLoop 和事件相关面试题

Docker 容器环境下 Node.js 应用程序的优雅退出

Node.js 服务 Docker 容器化应用实践

Node.js 是什么?我为什么选择它?

分享 10 道 Nodejs 进程相关面试题

Node.js进阶之进程与线程

Node.js 中的缓冲区(Buffer)究竟是什么?

Node.js 内存管理和 V8 垃圾回收机制

浅谈 Node.js 模块机制及常见面试问题解答

Node.js 项目 TypeScript 改造指南

Node.js 项目 TypeScript 改造指南

在看点这里

Node.js 项目 TypeScript 改造指南

本文分享自微信公众号 - Nodejs技术栈(NodejsRoadmap)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这