将TypeScript添加到你现有的Rails应用程序中
你是否想过在你的应用程序的前端尝试使用TypeScript?这很诱人,但一想到要移植所有现有的JS,就觉得太累了。但是,如果你能逐步将 TypeScript 引入你现有的应用程序,只在有意义的地方使用它,那会怎样?在这篇文章中,我将介绍如何做到这一点。
TypeScript是一个强类型的JavaScript超集,由微软开发和维护。强类型有助于你写出更干净的代码,并在开发过程中更早地检测和修复潜在的错误。
由于TypeScript是JavaScript的超集,任何现有的JavaScript程序也是一个有效的TypeScript程序。这意味着TypeScript可以与任何现有的JavaScript代码无缝对接。这也意味着从JavaScript迁移到TypeScript可以逐步完成。
尽管TypeScript和JavaScript可以很好地合作,但在计划迁移时有一些重要的因素需要考虑。这篇文章将给你一个坚实的基础,所以你可以决定迁移是否适合你的项目。
在你的项目中添加TypeScript
从JavaScript迁移到TypeScript时,需要记住的基本事项是,后者是以.ts ,而不是.js 。不过,你可以让JavaScript文件通过TypeScript编译器处理,以避免用TypeScript重写所有代码。
在你进一步行动之前,确保你的文本编辑器或IDE被配置成可以与TypeScript一起工作。使用TypeScript的一个关键优势是,在编译代码之前,可以在你的编辑器中报告错误,同时还有智能代码完成。Visual Studio Code对TypeScript语言有内置支持,所以如果你使用这个编辑器,你不需要做任何事情。否则,应该很容易找到一个第三方插件,将TypeScript支持添加到你的编辑器。
一旦你设置了你的编辑器,下一步就是将TypeScript编译器添加到你的项目中。你可以通过npm 。
$ npm install typescript --save-dev
上面的命令将TypeScript编译器添加到你的项目中,可以通过npx tsc 命令访问。你也可以在全局范围内安装编译器,使tsc 命令在任何地方都可以访问,但本地安装应该是首选,这样在不同的机器上构建是可以重复的。
一旦你安装了TypeScript,你需要为你的项目创建一个配置文件。TypeScript使用一个tsconfig.json 文件来管理你的项目的选项,比如要包括的文件和你想执行的检查类型。这里有一个最小的配置,你可以开始使用。
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"outDir": "dist"
"allowJs": true,
},
"include": ["./src/**/*"]
}
在JSON文件中最常见的配置选项是compilerOptions 和include 属性。后者用于指定一个文件名或模式的数组,以包含在相对于tsconfig.json 文件的程序中。它支持通配符来形成glob模式,其中可以包括也可以不包括文件扩展名。如果省略了文件扩展名(如上),只包括支持的扩展名。.ts,.tsx,.d.ts 默认情况下,如果compilerOptions.allowJs 被设置为true ,则有.js 和.jsx 。
然而,compilerOptions 属性允许你决定编译器在处理构建时应该有多宽松或严格。这是你大部分配置的地方。下面是上面指定的每个选项的作用。
- 上面的
target属性允许你将较新的JavaScript语法转换为较旧的版本,如ECMAScript 5。 allowJs导致TypeScript编译器接受JavaScript文件(包括导入)。这是一种通过允许 和 文件与现有的JavaScript文件一起存在来逐步转换为TypeScript的方法。.ts.tsxoutDir导致构建的文件被输出到 文件夹中。dist
在这一点上,你可以使用npx tsc --watch ,在观察模式下运行编译器,它将编译源文件并将输出到dist文件夹。
用Webpack编译TypeScript
有很多方法可以将TypeScript与现有的Webpack配置整合起来。如果你使用babel-loader包来转译JavaScript文件,你可以添加@babel/preset-typescript预设来生成JavaScript文件,并添加Fork TS Checker Webpack Plugin包来运行TypeScript类型检查器,以便在出现类型错误时构建失败。
首先,用npm 安装这两个包。
$ npm install fork-ts-checker-webpack-plugin @babel/preset-typescript --save-dev
然后,更新你的Webpack配置文件,以大致反映以下内容。
webpack.config.js
const path = require("path");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const typescript = {
test: /\.(ts|js)$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-typescript"],
},
},
],
};
module.exports = {
entry: {
main: "./src/main.ts",
},
resolve: {
extensions: [".ts", ".js"],
},
module: {
rules: [typescript],
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].bundle.js",
},
plugins: [new ForkTsCheckerWebpackPlugin()],
};
在这一点上,Webpack将负责转译和类型检查文件,如果有任何错误,将导致构建失败。

将TypeScript添加到Rails + Webpacker项目中
下面的说明假设你已经有一个使用Webpacker 5.1或更高版本的Rails 6项目。你需要做的第一件事是使用下面的命令向你的项目添加TypeScript支持。
$ bundle exec rails webpacker:install:typescript
这可以确保你的TypeScript代码使用Babel进行转译(通过@babel/preset-typescript 包)。如果你想启用类型检查作为Webpack构建过程的一部分,你需要手动安装Fork TS Checker Webpack插件包。
$ yarn add --dev fork-ts-checker-webpack-plugin
然后,更新你的config/webpack/development.js 文件,如下所示。
development.js
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const path = require("path");
environment.plugins.append(
"ForkTsCheckerWebpackPlugin",
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.resolve(__dirname, "../../tsconfig.json"),
},
async: false,
})
);
转移到TypeScript
基本上有两种主要方式可以将现有项目过渡到TypeScript。第一种方法是用TypeScript重写整个项目。这并不像听起来那么难,因为它只涉及到将文件扩展名改为.ts 或.tsx ,并修复编译器发出的任何类型错误。这种方法的主要问题是,你可能会遇到成百上千的错误(取决于项目的大小和你的TypeScript配置的严格程度),而且你将不得不暂停新功能的开发,直到你完成迁移,这可能需要大量的时间。
第二种更实用的方法是在代码库中支持JavaScript和TypeScript文件的混合,并逐步将文件切换到TypeScript。由于TypeScript编译器的灵活性(通过allowJs 选项),这个过程应该是轻而易举的。你所需要做的就是将TypeScript添加到你的构建过程中,并设置一个基本的配置,比如本文前面介绍的配置。之后,你需要确保任何新的功能都在TypeScript中实现,而现有的代码则以增量的方式转移过来。
tsconfig.json 一旦你通过include 或files 文件中的属性定义了你的依赖图,编译器将开始对你代码库中的任何TypeScript文件进行类型检查。你也可以通过checkJs 编译器选项启用JavaScript文件的类型检查。这允许你使用JSDoc将类型注释添加到你的JavaScript文件中,所以你可以开始在你的应用程序中使用类型,但不需要完全提交TypeScript。
当你准备提交时,你需要将.js 或.jsx 文件分别重命名为.ts 或.tsx ,并开始使用 TypeScript 语法来定义类型,而不是 JSDoc。例如,假设你有一个add.js 文件,代码如下。
add.js
function add(x, y) {
return x + y;
}
export default add;
在这一点上,所有的东西都被隐含地打成了any 。这意味着TypeScript不会对这些值执行任何类型检查。你可以选择使用JSDoc注释对普通的JavaScript文件进行类型检查,如下所示。
添加...js
/**
* @param {number} x
* @param {number} y
* @returns {number}
*/
function add(x, y) {
return x + y;
}
export default add;
如果add ,现在TypeScript编译器将报告错误,例如当一个字符串被传递为参数而不是一个数字。
在这一点上,你可能已经准备好将文件转移到TypeScript。只需将文件扩展名改为.ts ,并将JSDoc的注释翻译成TypeScript的语法。
add.ts
function add(x: number, y: number): number {
return x + y;
}
export default add;
export default add; ````
有了这个策略,你就能够逐渐迁移到TypeScript,而没有太多的摩擦。这种方法的主要注意事项是,由于缺乏动力,大量的代码有可能保持不类型化。
与第三方库合作
以上一节所述的方式迁移到 TypeScript,对你的应用程序代码来说是很好的,但依赖一些第三方库的情况并不少见,这些库可能需要一些额外的配置,以便保留使用 TypeScript 的好处。
用TypeScript编写的库应该是开箱即用的,不需要做任何手脚。当使用一个兼容的编辑器时,你将能够看到库所暴露的一切,以及函数参数和返回值的类型。编译器将确保你使用正确的类型,如果你不这样做,编译就会失败。
然而,对于用JavaScript编写的包(大多数),TypeScript不能自动确定类型是什么,所以它隐式地将any 类型分配给整个库。这是有问题的,因为你没有得到任何类型安全的any 类型,所以即使你使用一个不存在的方法,编译器也不会抱怨。
import * as lodash from "lodash";
// lodash.sneeze() is not a function
lodash.sneeze();
如果你打开了noImplicitAny编译器标志(推荐),编译会失败,出现类似于下面的错误。这实质上意味着TypeScript不知道哪些类型对Lodash库有效。
解决这个问题的方法主要有两种,从上面的错误信息可以看出。我们先来谈谈声明文件的方法。它涉及到创建一个.d.ts 文件,在其中描述另一个文件或包的类型定义。例如,你可以在你的源代码文件夹中创建一个main.d.ts 文件,内容如下。
main.d.ts
declare module "lodash" {
function sneeze(): string;
}
这个文件说明lodash模块暴露了一个返回字符串的sneeze 函数。如果你再次运行构建,它会被编译,因为TypeScript相信声明文件中的类型定义是正确的,而它没有办法检查它们是否真的准确。当然,代码会抛出一个运行时错误,因为sneeze 方法并不存在。
如果你试图使用库中的其他方法,构建将再次失败,直到其类型定义被添加到声明文件中。这是向缺乏类型的第三方库添加类型的方法之一,使得编译器有可能对代码提供更强的保证。
向普通JavaScript包添加类型的第二种方法是通过DefinitelyTyped包,它是一个由社区提供的类型定义文件库。如果你想在你的项目中使用一个流行的JavaScript库,很有可能该库的类型定义已经被贡献到了这个仓库中。这意味着你可以通过@types 范围内的npm ,轻松地将它们引入你的项目。例如,lodash包的类型可以通过以下命令添加。
$ npm install --save @types/lodash
像其他npm包一样,类型声明包也安装在node_modules 文件夹中。在其中,你会发现一个包含所有类型的@types 文件夹。运行上面的命令后,你会在@types 里面找到一个lodash 文件夹,其中包含了几个文件,包含了所有lodash方法的类型信息。TypeScript编译器理解这个约定,所以它将自动识别类型,而不需要你的干预。
在这一点上,你可以删除main.d.ts 中的模块定义,并再次构建该项目。正如你从上面的图片中看到的那样,它正确地报告说sneeze 不存在。如果我们使用一个正确的方法,如ceil ,并加上正确的参数,它就能正常编译。另外,你会在编辑器中得到那种甜蜜的自动完成的好处,即类型注释。
请注意,DefinitelyTyped包中的类型定义是由社区提供的,在大多数情况下不是由库的作者提供。这意味着,你可能会不时地遇到丢失或不正确的定义。让我们来谈谈如果出现这种情况该怎么做。
合并声明
TypeScript编译器允许将两个或多个类型合并为一个定义,前提是它们具有相同的名称。这个合并的定义保留了两个原始声明的特性。这里有一个例子,应该能让这个概念更容易理解。
interface Person {
name: string;
}
interface Person {
name: boolean;
age: number;
}
const jack: Person = {
name: "Jack",
age: 20,
};
console.log(jack);
在这里,两个Person 的声明都被合并为一个定义,所以jack 对象包含了两个接口的所有属性。这里需要注意的一点是,同名的后续属性声明必须具有相同的类型。
interface Person {
name: string;
}
interface Person {
name: string; // works fine
age: number;
}
interface Person {
name: boolean; // throws an error
age: number;
}
这一点本身可能看起来不是很有用,但如果你想扩展一个不是由你定义的类型,它就会派上用场。例如,假设sneeze 方法真的存在于lodash中,但目前在@types/lodash 包中却没有。你可以在.d.ts 文件中通过声明合并来添加它。
main.d.ts
import * as _ from "lodash";
declare module "lodash" {
interface LoDashStatic {
sneeze(): string;
}
}
要扩展一个模块,你需要导入它并使用declare module 来进入模块内部。如果你看一下@types/lodash包,你会发现所有的方法都是在LoDashStatic 接口上定义的。要添加一个新的方法,你需要做的就是在lodash 模块中再次声明该接口,并为该函数添加类型定义。此时,如果你试图在你的代码中使用sneeze 方法,它将和所有其他存在于原始LoDashStatic 接口的方法一起被编译。
这样一来,你就可以快速修复缺失类型的错误,而不必等待软件包的更新。一旦相关的@types 包被更新和发布,你就可以删除.d.ts 文件中的自定义定义,并通过npm 更新包,其他一切都应该继续工作。
结论
在同一个代码库中使用JavaScript和TypeScript可能需要一些时间来适应,但是类型定义和声明合并的知识应该会让事情变得更容易。一旦你把整个代码库转换为TypeScript,你应该提高编译器的严格程度,以获得更大的安全性。此外,还可以查看runtypes和io-ts等库,对你的静态类型进行运行时验证。