TypeScript 并非凭空产生。它的构建充分考虑了 JavaScript 生态系统,而且目前已经存在大量的 JavaScript 代码。将 JavaScript 代码库转换为 TypeScript 虽然有些繁琐,但通常并不困难。在本教程中,我们将探讨如何开始这一过程。我们假设您已经阅读了足够的手册内容,可以编写新的 TypeScript 代码。
如果您打算转换一个 React 项目,我们建议您先查看 React 转换指南。
设置您的目录
如果您正在编写纯 JavaScript,很可能您是直接运行 JavaScript 的,即您的 .js 文件位于 src、lib 或 dist 目录中,然后按需运行。
如果是这种情况,您编写的文件将作为 TypeScript 的输入,而您将运行它生成的输出。在 JS 到 TS 的迁移过程中,我们需要分离输入文件,以防止 TypeScript 覆盖它们。如果您的输出文件需要存放在特定目录中,那么这就是您的输出目录。
您可能还对 JavaScript 执行了一些中间步骤,例如打包或使用像 Babel 这样的其他编译器。在这种情况下,您可能已经设置好了类似的文件结构。
从现在起,我们假设您的目录结构如下所示
projectRoot├── src│ ├── file1.js│ └── file2.js├── built└── tsconfig.json
如果您在 src 目录之外还有一个 tests 文件夹,您可能需要在 src 中配置一个 tsconfig.json,在 tests 中也配置一个。
编写配置文件
TypeScript 使用名为 tsconfig.json 的文件来管理项目的选项,例如您希望包含哪些文件,以及希望执行何种类型的检查。让我们为项目创建一个最基本的配置
json{"compilerOptions": {"outDir": "./built","allowJs": true,"target": "es5"},"include": ["./src/**/*"]}
这里我们向 TypeScript 指定了几件事
- 读取
src目录中它能理解的所有文件(使用include)。 - 接受 JavaScript 文件作为输入(使用
allowJs)。 - 将所有输出文件发送到
built目录(使用outDir)。 - 将较新的 JavaScript 结构转换(降级)到较旧的版本,如 ECMAScript 5(使用
target)。
此时,如果您尝试在项目根目录下运行 tsc,应该会在 built 目录中看到输出文件。built 中的文件布局应与 src 中的布局完全一致。现在 TypeScript 应该已经可以在您的项目中工作了。
早期收益
即使是在现阶段,您也能从 TypeScript 对项目的理解中获得巨大收益。如果您打开像 VS Code 或 Visual Studio 这样的编辑器,您会发现通常可以获得一些工具支持,如自动补全。您还可以通过以下选项捕获特定的错误:
noImplicitReturns,它可以防止您忘记在函数末尾返回值。noFallthroughCasesInSwitch,如果您不希望忘记switch块中case之间的break语句,这个选项很有用。
TypeScript 还会针对无法访问的代码和标签发出警告,您可以分别通过 allowUnreachableCode 和 allowUnusedLabels 来禁用这些警告。
集成构建工具
您的流水线中可能还有更多的构建步骤。也许您需要将某些内容拼接到每个文件中。每个构建工具都不同,但我们将尽力涵盖其要点。
Gulp
如果您以某种方式使用 Gulp,我们有关于结合 TypeScript 使用 Gulp,以及与 Browserify、Babelify 和 Uglify 等常用构建工具集成的教程。您可以在那里阅读更多信息。
Webpack
Webpack 集成非常简单。您可以使用 TypeScript 加载器 ts-loader,结合 source-map-loader 以获得更轻松的调试体验。只需运行
shellnpm install ts-loader source-map-loader
并将以下选项合并到您的 webpack.config.js 文件中
jsmodule.exports = {entry: "./src/index.ts",output: {filename: "./dist/bundle.js",},// Enable sourcemaps for debugging webpack's output.devtool: "source-map",resolve: {// Add '.ts' and '.tsx' as resolvable extensions.extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"],},module: {rules: [// All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.{ test: /\.tsx?$/, loader: "ts-loader" },// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.{ test: /\.js$/, loader: "source-map-loader" },],},// Other options...};
需要注意的是,ts-loader 必须在处理 .js 文件的任何其他加载器之前运行。
您可以在我们的关于 React 和 Webpack 的教程中看到一个使用 Webpack 的示例。
转向 TypeScript 文件
此时,您可能准备好开始使用 TypeScript 文件了。第一步是将您的一个 .js 文件重命名为 .ts。如果您的文件使用了 JSX,则需要将其重命名为 .tsx。
完成了这一步?太棒了!您已成功将一个文件从 JavaScript 迁移到 TypeScript!
当然,这可能会让您感觉有些不妥。如果您在支持 TypeScript 的编辑器中打开该文件(或者运行 tsc --pretty),您可能会在某些行看到红色波浪线。您应该像对待 Microsoft Word 等编辑器中的红线一样对待它们。TypeScript 仍然会转换您的代码,就像 Word 仍然允许您打印文档一样。
如果这听起来对您来说太宽松了,您可以收紧该行为。例如,如果您不希望 TypeScript 在出现错误时编译成 JavaScript,可以使用 noEmitOnError 选项。从这个意义上说,TypeScript 有一个严格程度的刻度盘,您可以根据需要将其调高。
如果您计划使用可用的更严格的设置,最好现在就开启它们(请参阅下方的获取更严格的检查)。例如,如果您不希望 TypeScript 在您未明确说明的情况下隐式推断类型为 any,可以在开始修改文件之前使用 noImplicitAny。虽然这可能会让您感到不知所措,但长期的收益会更快显现出来。
剔除错误
正如我们所提到的,转换后出现错误信息并不出乎意料。重要的是要逐一查看这些错误并决定如何处理。通常这些是合法的 Bug,但有时您需要向 TypeScript 更好地解释您要实现的目标。
从模块导入
您刚开始可能会遇到一堆像 Cannot find name 'require'. 和 Cannot find name 'define'. 这样的错误。在这些情况下,很可能您正在使用模块。虽然您可以通过编写以下代码让 TypeScript 相信这些是存在的
ts// For Node/CommonJSdeclare function require(path: string): any;
或者
ts// For RequireJS/AMDdeclare function define(...args: any[]): any;
但更好的做法是去掉这些调用,改用 TypeScript 的导入语法。
首先,您需要通过设置 TypeScript 的 module 选项来启用某个模块系统。有效选项为 commonjs、amd、system 和 umd。
如果您有以下 Node/CommonJS 代码
jsvar foo = require("foo");foo.doStuff();
或者以下 RequireJS/AMD 代码
jsdefine(["foo"], function (foo) {foo.doStuff();});
那么您应该编写以下 TypeScript 代码
tsimport foo = require("foo");foo.doStuff();
获取声明文件
如果您开始转换为 TypeScript 导入,很可能会遇到类似 Cannot find module 'foo'. 的错误。这里的问题是您可能没有用于描述库的声明文件。幸运的是,这非常简单。如果 TypeScript 对像 lodash 这样的包有意见,您只需编写
shellnpm install -S @types/lodash
如果您使用的模块选项不是 commonjs,则需要将 moduleResolution 选项设置为 node。
之后,您将能够无误地导入 lodash,并获得准确的补全。
从模块导出
通常,从模块导出涉及将属性添加到像 exports 或 module.exports 这样的值上。TypeScript 允许您使用顶级导出语句。例如,如果您像这样导出一个函数
jsmodule.exports.feedPets = function (pets) {// ...};
您可以将其写为如下形式
tsexport function feedPets(pets) {// ...}
有时您会完全覆盖 exports 对象。这是一个常见的模式,人们用它来使他们的模块可以立即被调用,如下面的代码片段所示
jsvar express = require("express");var app = express();
您可能以前是这样写的
jsfunction foo() {// ...}module.exports = foo;
在 TypeScript 中,您可以使用 export = 结构来建模。
tsfunction foo() {// ...}export = foo;
参数过多/过少
有时您会发现自己调用函数时参数过多或过少。通常这是一个错误,但在某些情况下,您可能声明了一个使用 arguments 对象而不是写出任何参数的函数
jsfunction myCoolFunction() {if (arguments.length == 2 && !Array.isArray(arguments[1])) {var f = arguments[0];var arr = arguments[1];// ...}// ...}myCoolFunction(function (x) {console.log(x);},[1, 2, 3, 4]);myCoolFunction(function (x) {console.log(x);},1,2,3,4);
在这种情况下,我们需要使用 TypeScript 通过函数重载来告知任何调用者 myCoolFunction 可以以何种方式被调用。
tsfunction myCoolFunction(f: (x: number) => void, nums: number[]): void;function myCoolFunction(f: (x: number) => void, ...nums: number[]): void;function myCoolFunction() {if (arguments.length == 2 && !Array.isArray(arguments[1])) {var f = arguments[0];var arr = arguments[1];// ...}// ...}
我们为 myCoolFunction 添加了两个重载签名。第一个检查声明 myCoolFunction 接受一个函数(该函数接受一个 number),然后是一个 number 列表。第二个表示它也接受一个函数,然后使用剩余参数(...nums)来声明在此之后的任何数量的参数都必须是 number 类型。
依次添加的属性
有些人觉得创建对象并立即在其后添加属性在美学上更令人愉悦,如下所示
jsvar options = {};options.color = "red";options.volume = 11;
TypeScript 会说您不能赋值给 color 和 volume,因为它最初推断 options 的类型为 {},而该类型没有任何属性。如果您将声明移入对象字面量本身,就不会有错误
tslet options = {color: "red",volume: 11,};
您也可以定义 options 的类型,并对对象字面量进行类型断言。
tsinterface Options {color: string;volume: number;}let options = {} as Options;options.color = "red";options.volume = 11;
或者,您可以直接说 options 的类型是 any,这是最容易做到的,但收益最少。
any、Object 和 {}
您可能想使用 Object 或 {} 来表示一个值可以具有任何属性,因为 Object 在大多数目的上是最通用的类型。然而,any 实际上才是您在这些情况下想要使用的类型,因为它是最灵活的类型。
例如,如果某个东西的类型被设为 Object,您将无法对其调用像 toLowerCase() 这样的方法。更通用通常意味着您对该类型能做的事情更少,但 any 很特别,它是最通用的类型,同时又允许您对其进行任何操作。这意味着您可以调用它、构造它、访问其属性等。但请记住,每当您使用 any 时,您都会失去 TypeScript 提供的绝大部分错误检查和编辑器支持。
如果需要在 Object 和 {} 之间做出决定,您应该优先选择 {}。虽然它们在大多数情况下是相同的,但从技术上讲,在某些深奥的情况下,{} 是比 Object 更通用的类型。
获取更严格的检查
TypeScript 附带了某些检查,旨在为您提供更高的安全性和程序分析。一旦您将代码库转换为 TypeScript,就可以开始启用这些检查以获得更高的安全性。
禁止隐式 any
在某些情况下,TypeScript 无法确定某些类型应该是什么。为了尽可能宽容,它会决定使用 any 类型来代替。虽然这对于迁移非常有用,但使用 any 意味着您没有得到任何类型安全,也不会得到在其他地方所能获得的相同工具支持。您可以使用 noImplicitAny 选项告诉 TypeScript 标记这些位置并发出错误。
严格的 null 和 undefined 检查
默认情况下,TypeScript 假设 null 和 undefined 属于每种类型的域。这意味着任何声明为 number 类型的变量都可能是 null 或 undefined。由于 null 和 undefined 是 JavaScript 和 TypeScript 中 Bug 的常见来源,TypeScript 提供了 strictNullChecks 选项,让您免于担心这些问题。
当启用 strictNullChecks 时,null 和 undefined 分别拥有自己的类型,即 null 和 undefined。每当某项内容可能为 null 时,您可以使用原始类型的联合类型。例如,如果某项内容可能是 number 或 null,您可以将其类型写为 number | null。
如果您有一个值,TypeScript 认为它可能为 null/undefined,但您很确定它不是,可以使用后缀 ! 操作符来告知它。
tsdeclare var foo: string[] | null;foo.length; // error - 'foo' is possibly 'null'foo!.length; // okay - 'foo!' just has type 'string[]'
需要提醒的是,使用 strictNullChecks 时,您的依赖项可能也需要更新以使用 strictNullChecks。
禁止 this 的隐式 any
当您在类之外使用 this 关键字时,它默认类型为 any。例如,想象一个 Point 类,以及一个我们希望添加为方法的函数
tsclass Point {constructor(public x, public y) {}getDistance(p: Point) {let dx = p.x - this.x;let dy = p.y - this.y;return Math.sqrt(dx ** 2 + dy ** 2);}}// ...// Reopen the interface.interface Point {distanceFromOrigin(): number;}Point.prototype.distanceFromOrigin = function () {return this.getDistance({ x: 0, y: 0 });};
这与我们上面提到的问题相同——我们很容易拼错 getDistance 而没有收到错误。因此,TypeScript 提供了 noImplicitThis 选项。当该选项被设置时,如果 this 在没有显式(或推断)类型的情况下被使用,TypeScript 将发出错误。解决方法是使用 this 参数在接口或函数本身中提供一个显式类型
tsPoint.prototype.distanceFromOrigin = function (this: Point) {return this.getDistance({ x: 0, y: 0 });};