从 JavaScript 迁移
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如果你的 tests 文件夹在 src 目录之外,你可能在 src 中有一个 tsconfig.json,在 tests 中也有一个。
编写配置文件
TypeScript 使用名为 tsconfig.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,我们有一个关于 将 Gulp 与 TypeScript 结合使用 的教程,以及与常见构建工具(如 Browserify、Babelify 和 Uglify)集成的教程。 你可以在那里阅读更多内容。
Webpack
Webpack 集成相当简单。 你可以使用 ts-loader(一个 TypeScript 加载器),并结合 source-map-loader 以便于调试。 只需运行
npm install ts-loader source-map-loader然后将以下选项合并到你的 webpack.config.js 文件中:
module.exports = {
entry: "./src/index.ts",
output: {
filename: "./dist/bundle.js",
},
// 启用 sourcemaps 以调试 webpack 的输出。
devtool: "source-map",
resolve: {
// 将 '.ts' 和 '.tsx' 添加为可解析的扩展名。
extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"],
},
module: {
rules: [
// 所有带有 '.ts' 或 '.tsx' 扩展名的文件都将由 'ts-loader' 处理。
{ test: /\.tsx?$/, loader: "ts-loader" },
// 所有输出的 '.js' 文件都将由 'source-map-loader' 重新处理 sourcemaps。
{ test: /\.js$/, loader: "source-map-loader" },
],
},
// 其他选项...
};需要注意的是,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 相信这些是存在的:
// 对于 Node/CommonJS
declare function require(path: string): any;或
// 对于 RequireJS/AMD
declare function define(...args: any[]): any;但更好的做法是去掉这些调用,并使用 TypeScript 的导入语法。
首先,你需要通过设置 TypeScript 的 module 选项来启用某个模块系统。 有效的选项是 commonjs、amd、system 和 umd。
如果你有以下 Node/CommonJS 代码:
var foo = require("foo");
foo.doStuff();或以下 RequireJS/AMD 代码:
define(["foo"], function (foo) {
foo.doStuff();
});那么你应该编写以下 TypeScript 代码:
import foo = require("foo");
foo.doStuff();获取声明文件
如果你开始转换为 TypeScript 导入,你可能会遇到诸如 Cannot find module 'foo'. 之类的错误。 问题在于你可能没有声明文件来描述你的库。 幸运的是,这很容易解决。 如果 TypeScript 抱怨像 lodash 这样的包,你只需运行
npm install -S @types/lodash如果你使用的模块选项不是 commonjs,你需要将你的 moduleResolution 选项设置为 node。
之后,你将能够毫无问题地导入 lodash,并获得准确的代码补全。
从模块导出
通常,从模块导出涉及向 exports 或 module.exports 这样的值添加属性。 TypeScript 允许你使用顶级的 export 语句。 例如,如果你像这样导出一个函数:
module.exports.feedPets = function (pets) {
// ...
};你可以将其写为:
export function feedPets(pets) {
// ...
}有时你会完全覆盖 exports 对象。 人们常用这种模式使他们的模块立即可调用,就像这样:
var express = require("express");
var app = express();你可能之前是这样写的:
function foo() {
// ...
}
module.exports = foo;在 TypeScript 中,你可以使用 export = 结构来建模。
function foo() {
// ...
}
export = foo;参数过多/过少
有时你会发现自己调用函数时参数过多/过少。 通常这是一个 bug,但在某些情况下,你可能声明了一个使用 arguments 对象而不是写出任何参数的函数:
function 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 可以以哪些方式调用。
function 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 类型。
按顺序添加属性
有些人觉得创建一个对象并立即添加属性在美学上更令人愉悦,如下所示:
var options = {};
options.color = "red";
options.volume = 11;TypeScript 会说你不能赋值给 color 和 volume,因为它首先将 options 的类型推断为 {},它没有任何属性。 如果你改为将声明移到对象字面量本身中,就不会出现错误:
let options = {
color: "red",
volume: 11,
};你也可以定义 options 的类型,并在对象字面量上添加类型断言。
interface 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 意味着你没有获得任何类型安全性,并且你不会获得其他地方会得到的相同工具支持。 你可以告诉 TypeScript 标记这些位置,并使用 noImplicitAny 选项报错。
严格的 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 的值,但你知道它不会,你可以使用后缀 ! 操作符来告诉它。
declare var foo: string[] | null;
foo.length; // 错误 - 'foo' 可能为 'null'
foo!.length; // 没问题 - 'foo!' 的类型就是 'string[]'请注意,在使用 strictNullChecks 时,你的依赖项可能也需要更新以使用 strictNullChecks。
禁止 this 的隐式 any
当你在类外部使用 this 关键字时,它的默认类型是 any。 例如,想象一个 Point 类,以及一个我们希望作为方法添加的函数:
class 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);
}
}
// ...
// 重新打开接口。
interface Point {
distanceFromOrigin(): number;
}
Point.prototype.distanceFromOrigin = function () {
return this.getDistance({ x: 0, y: 0 });
};这与我们上面提到的问题相同——我们很容易拼错 getDistance 而不会得到错误。 因此,TypeScript 提供了 noImplicitThis 选项。 当设置该选项时,当 this 在没有显式(或推断)类型的情况下使用时,TypeScript 将发出错误。 解决方法是使用 this 参数在接口或函数本身中给出显式类型:
Point.prototype.distanceFromOrigin = function (this: Point) {
return this.getDistance({ x: 0, y: 0 });
};