相对于 JavaScript,TypeScript 增加了类型系统。通过静态检查,开发者可以更早地发现语法错误,同时类型标注也带来了清晰的文档。这篇文章记录了几个关于 React 项目从 JavaScript 迁移到 TypeScript 笔者处理过的一些问题,这些问题虽然并不复杂,但是如果在迁移的过程中遇到了,也需要花上不少的时间去调查和寻找解决方案。
首先推荐阅读微软的一篇 React 项目迁移文档。
简单总结一下,整个迁移的过程可以分为下面两类操作:
- 配置 TypeScript 编译器。
- 将 JavaScript 文件转化为 TypeScript 文件。
配置编译器
1. 安装依赖
依赖可以分为两类,一类是 TypeScript 编译工具,另一类是第三方依赖的类型声明文件(declarations)。
安装编译工具,TypeScript 的主流 loader 有 ts-loader 和 awesome-typescript-loader.
npm install --save-dev typescript ts-loader source-map-loader
安装第三方依赖类型文件,下面的例子只包括了 react 和 react-dom,其他依赖声明可以根据自己项目进行安装。
npm install --save -D @types/react @types/react-dom
如果最终发布的内容是会被其他项目引用的工具,应该把 @types 包括在发布的文件当中,即 npm i @types/xxx
,如果发布的内容不会被其他项目引用,那么安装为 devDependencies 即可,可以参考 stackoverflow 上的这篇讨论。
如果项目开发时使用了热更新(HMR),类型检查会报错 module.hot
类型问题,需要安装 @types/webpack-env
来解决这个类型问题。
2. webpack 配置更新
- 添加 resolve 的文件后缀:
resolve: {
// changed from extensions: [".js", ".jsx"]
extensions: [".ts", ".tsx", ".js", ".jsx"]
},
- 使用 ts-loader 处理 js, jsx, ts, tsx 文件:
module: {
rules: [
// changed from { test: /\.jsx?$/, use: { loader: 'babel-loader' } },
{
test: /\.(t|j)sx?$/,
use: {
loader: ts-loader',
options: { // options for spped up compilation
transpileOnly: true
}
}
},
// addition - add source-map support
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
]
},
项目冷启动开发时(npm start),如果顺序进行类型检查和编译,会花费不少的时间。我们可以另开一个线程来进行类型检查,从而节约时间。fork-ts-checker-webpack-plugin 就是这样一个 webpack plugin,非常推荐使用。
3. 添加 ts 编译器配置
// tsconfig.js
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"strictNullChecks": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"includes": [
"./src/",
]
}
在之前的打包配置里面,babel 实现的功能主要有两项,包括:编译 jsx 语法(preset-react)和 将晚进的语法编译为 es5 语法(preset-env),从而实现更好的兼容性。而 TypeScript 编译器同样可以实现这些操作,那么如果 babel 配置没有其他特殊操作(比如编译其他特殊语法),那么完全可以使用 TypeScript loader 替代 babel。
tsconfig 文件里面可以配置编译器的各种选项。如果类型检查的时候,因为引入的组件没有 default export (通过 module.exports 输出),可能会报错,那么配置allowSyntheticDefaultImport
可以让 ts 忽略引用报错。
文件转换
源代码文件
从 .js 迁移到 .ts,主要是添加各种类型声明。
一些小 tips:
- 事件类型(event):可以使用 React 事件注释:
export type InputEvent = React.ChangeEvent<HTMLInputElement>;
export type ButtonEvent = React.MouseEvent<HTMLButtonElement>;
- 组件类型(element):组件可以用
JSX.Element
标注。 - 组件方法声明问题,在 React 组件里面,有的时候会直接在 constructor 里面对创建的对象绑定方法:
constructor(props) {
...
this.foo = () => {
...
}
}
而下面这种方式是将方法声明给了该组件 class 的 prototype,然后在 constructor 里面绑定新建对象。
constructor(props) {
...
this.foo = this.foo.bind(this);
}
foo() {
...
}
这两种写法对于程序运行没有实际的区别,但是第一种方式声明的方法并不在 prototype 上,所以类型检查无法将生成对象的方法和 class 方法对应起来,会报错。解决方法是按照第二种写法,将方法绑定到 class prototype 上即可。
样式文件
对于预处理样式(比如 stylus,sass,less等),需要相应的 declarations 文件,通过配置 css-modules-typescript-loader
可以帮助我们自动生成 d.ts 文件。
- 安装 css-modules-typescript-loader,在 webpack 配置其在 css-loader 之后处理样式文件。
- 创建一个 declaration 文件,声明预处理样式文件命名格式,因为我们的项目文件名字采用 .cm.styl 后缀表示,所以声明如下:
// declarations.d.ts
declare module '*.cm.styl';