从 JavaScript 迁移

TypeScript 并不存在于真空中。它是在考虑 JavaScript 生态系统的情况下构建的,今天存在很多 JavaScript。将 JavaScript 代码库转换为 TypeScript 虽然有些乏味,但通常并不具有挑战性。在本教程中,我们将了解如何开始。我们假设你已经阅读了足够多的手册来编写新的 TypeScript 代码。

¥TypeScript doesn’t exist in a vacuum. It was built with the JavaScript ecosystem in mind, and a lot of JavaScript exists today. Converting a JavaScript codebase over to TypeScript is, while somewhat tedious, usually not challenging. In this tutorial, we’re going to look at how you might start out. We assume you’ve read enough of the handbook to write new TypeScript code.

如果你要转换 React 项目,我们建议你先查看 React 转换指南

¥If you’re looking to convert a React project, we recommend looking at the React Conversion Guide first.

设置目录

¥Setting up your Directories

如果你使用纯 JavaScript 编写,你很可能直接运行 JavaScript,你的 .js 文件位于 srclibdist 目录中,然后按需要运行。

¥If you’re writing in plain JavaScript, it’s likely that you’re running your JavaScript directly, where your .js files are in a src, lib, or dist directory, and then run as desired.

如果是这种情况,你编写的文件将用作 TypeScript 的输入,你将运行它产生的输出。在 JS 到 TS 的迁移过程中,我们需要分离输入文件以防止 TypeScript 覆盖它们。如果你的输出文件需要驻留在特定目录中,那么该目录就是你的输出目录。

¥If that’s the case, the files that you’ve written are going to be used as inputs to TypeScript, and you’ll run the outputs it produces. During our JS to TS migration, we’ll need to separate our input files to prevent TypeScript from overwriting them. If your output files need to reside in a specific directory, then that will be your output directory.

你可能还会在 JavaScript 上运行一些中间步骤,例如打包或使用其他转译器(如 Babel)。在这种情况下,你可能已经设置了这样的文件夹结构。

¥You might also be running some intermediate steps on your JavaScript, such as bundling or using another transpiler like Babel. In this case, you might already have a folder structure like this set up.

从现在开始,我们假设你的目录设置如下:

¥From this point on, we’re going to assume that your directory is set up something like this:

projectRoot ├── src │ ├── file1.js │ └── file2.js ├── built └── tsconfig.json

如果你在 src 目录之外有一个 tests 文件夹,那么你可能在 src 中有一个 tsconfig.json,在 tests 中也可能有一个。

¥If you have a tests folder outside of your src directory, you might have one tsconfig.json in src, and one in tests as well.

编写配置文件

¥Writing a Configuration File

TypeScript 使用一个名为 tsconfig.json 的文件来管理项目的选项,例如要包含哪些文件,以及要执行哪些类型的检查。让我们为我们的项目创建一个简单的系统:

¥TypeScript uses a file called tsconfig.json for managing your project’s options, such as which files you want to include, and what sorts of checking you want to perform. Let’s create a bare-bones one for our project:

json
{
"compilerOptions": {
"outDir": "./built",
"allowJs": true,
"target": "es5"
},
"include": ["./src/**/*"]
}

这里我们为 TypeScript 指定了一些东西:

¥Here we’re specifying a few things to TypeScript:

  1. 读入 src 目录(带有 include)中它理解的任何文件。

    ¥Read in any files it understands in the src directory (with include).

  2. 接受 JavaScript 文件作为输入(使用 allowJs)。

    ¥Accept JavaScript files as inputs (with allowJs).

  3. 触发 built(带有 outDir)中的所有输出文件。

    ¥Emit all of the output files in built (with outDir).

  4. 将较新的 JavaScript 结构转换为旧版本,如 ECMAScript 5(使用 target)。

    ¥Translate newer JavaScript constructs down to an older version like ECMAScript 5 (using target).

此时,如果你尝试在项目的根目录下运行 tsc,你应该会在 built 目录中看到输出文件。built 中的文件布局应该与 src 中的布局相同。你现在应该让 TypeScript 与你的项目一起工作。

¥At this point, if you try running tsc at the root of your project, you should see output files in the built directory. The layout of files in built should look identical to the layout of src. You should now have TypeScript working with your project.

早期福利

¥Early Benefits

即使在这一点上,你也可以从 TypeScript 理解你的项目中获得一些巨大的好处。如果你打开像 VS CodeVisual Studio 这样的编辑器,你会发现你经常可以获得一些工具支持,比如完成。你还可以使用以下选项捕获某些错误:

¥Even at this point you can get some great benefits from TypeScript understanding your project. If you open up an editor like VS Code or Visual Studio, you’ll see that you can often get some tooling support like completion. You can also catch certain bugs with options like:

TypeScript 还会警告无法访问的代码和标签,你可以分别使用 allowUnreachableCodeallowUnusedLabels 禁用它们。

¥TypeScript will also warn about unreachable code and labels, which you can disable with allowUnreachableCode and allowUnusedLabels respectively.

与构建工具集成

¥Integrating with Build Tools

你的管道中可能有更多构建步骤。也许你将某些内容连接到每个文件。每个构建工具都不同,但我们会尽力涵盖事物的要点。

¥You might have some more build steps in your pipeline. Perhaps you concatenate something to each of your files. Each build tool is different, but we’ll do our best to cover the gist of things.

Gulp

如果你以某种方式使用 Gulp,我们有一个关于 使用 Gulp 与 TypeScript 的教程,以及与 Browserify、Babelify 和 Uglify 等常见构建工具的集成。你可以在那里阅读更多。

¥If you’re using Gulp in some fashion, we have a tutorial on using Gulp with TypeScript, and integrating with common build tools like Browserify, Babelify, and Uglify. You can read more there.

Webpack

Webpack 集成非常简单。你可以使用 ts-loader,一个 TypeScript 加载器,结合 source-map-loader 来更容易调试。只需运行

¥Webpack integration is pretty simple. You can use ts-loader, a TypeScript loader, combined with source-map-loader for easier debugging. Simply run

shell
npm install ts-loader source-map-loader

并将以下选项合并到你的 webpack.config.js 文件中:

¥and merge in options from the following into your webpack.config.js file:

js
module.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 文件的加载程序之前运行。

¥It’s important to note that ts-loader will need to run before any other loader that deals with .js files.

你可以在我们的 React 和 Webpack 教程 中看到一个使用 Webpack 的例子。

¥You can see an example of using Webpack in our tutorial on React and Webpack.

移动到 TypeScript 文件

¥Moving to TypeScript Files

此时,你可能已准备好开始使用 TypeScript 文件。第一步是将其中一个 .js 文件重命名为 .ts。如果你的文件使用 JSX,则需要将其重命名为 .tsx

¥At this point, you’re probably ready to start using TypeScript files. The first step is to rename one of your .js files to .ts. If your file uses JSX, you’ll need to rename it to .tsx.

完成那一步了吗?很棒!你已成功将文件从 JavaScript 迁移到 TypeScript!

¥Finished with that step? Great! You’ve successfully migrated a file from JavaScript to TypeScript!

当然,这可能感觉不对。如果你在支持 TypeScript 的编辑器中打开该文件(或者如果你运行 tsc --pretty),你可能会在某些行上看到红色波浪线。你应该像在 Microsoft Word 等编辑器中考虑红色波浪线一样来考虑这些。TypeScript 仍会翻译你的代码,就像 Word 仍会让你打印文档一样。

¥Of course, that might not feel right. If you open that file in an editor with TypeScript support (or if you run tsc --pretty), you might see red squiggles on certain lines. You should think of these the same way you’d think of red squiggles in an editor like Microsoft Word. TypeScript will still translate your code, just like Word will still let you print your documents.

如果这听起来对你来说太宽松了,你可以收紧这种行为。例如,如果你不希望 TypeScript 在遇到错误时编译为 JavaScript,则可以使用 noEmitOnError 选项。从这个意义上说,TypeScript 有一个严格的刻度盘,你可以把那个旋钮调到你想要的最高。

¥If that sounds too lax for you, you can tighten that behavior up. If, for instance, you don’t want TypeScript to compile to JavaScript in the face of errors, you can use the noEmitOnError option. In that sense, TypeScript has a dial on its strictness, and you can turn that knob up as high as you want.

如果你计划使用更严格的可用设置,最好现在就启用它们(请参阅下面的 获得更严格的检查)。例如,如果你不希望 TypeScript 在你未明确说明的情况下静默推断类型的 any,则可以在开始修改文件之前使用 noImplicitAny。虽然这可能让人感到有些不知所措,但长期收益会更快地显现出来。

¥If you plan on using the stricter settings that are available, it’s best to turn them on now (see Getting Stricter Checks below). For instance, if you never want TypeScript to silently infer any for a type without you explicitly saying so, you can use noImplicitAny before you start modifying your files. While it might feel somewhat overwhelming, the long-term gains become apparent much more quickly.

剔除错误

¥Weeding out Errors

正如我们提到的,转换后收到错误消息并不意外。重要的是实际逐一检查并决定如何处理错误。这些通常是合法的错误,但有时你必须解释你试图对 TypeScript 做些什么。

¥Like we mentioned, it’s not unexpected to get error messages after conversion. The important thing is to actually go one by one through these and decide how to deal with the errors. Often these will be legitimate bugs, but sometimes you’ll have to explain what you’re trying to do a little better to TypeScript.

从模块导入

¥Importing from Modules

你可能会开始收到一堆错误,例如 Cannot find name 'require'.Cannot find name 'define'.。在这些情况下,你很可能正在使用模块。虽然你可以通过写出来让 TypeScript 相信这些存在

¥You might start out getting a bunch of errors like Cannot find name 'require'., and Cannot find name 'define'.. In these cases, it’s likely that you’re using modules. While you can just convince TypeScript that these exist by writing out

ts
// For Node/CommonJS
declare function require(path: string): any;

或者

¥or

ts
// For RequireJS/AMD
declare function define(...args: any[]): any;

最好摆脱这些调用并使用 TypeScript 语法进行导入。

¥it’s better to get rid of those calls and use TypeScript syntax for imports.

首先,你需要通过设置 TypeScript 的 module 选项来启用某些模块系统。有效选项为 commonjsamdsystemumd

¥First, you’ll need to enable some module system by setting TypeScript’s module option. Valid options are commonjs, amd, system, and umd.

如果你有以下 Node/CommonJS 代码:

¥If you had the following Node/CommonJS code:

js
var foo = require("foo");
foo.doStuff();

或以下 RequireJS/AMD 代码:

¥or the following RequireJS/AMD code:

js
define(["foo"], function (foo) {
foo.doStuff();
});

那么你将编写以下 TypeScript 代码:

¥then you would write the following TypeScript code:

ts
import foo = require("foo");
foo.doStuff();

获取声明文件

¥Getting Declaration Files

如果你开始转换为 TypeScript 导入,你可能会遇到像 Cannot find module 'foo'. 这样的错误。这里的问题是你可能没有声明文件来描述你的库。幸运的是,这很容易。如果 TypeScript 抗诉像 lodash 这样的包,你可以写

¥If you started converting over to TypeScript imports, you’ll probably run into errors like Cannot find module 'foo'.. The issue here is that you likely don’t have declaration files to describe your library. Luckily this is pretty easy. If TypeScript complains about a package like lodash, you can just write

shell
npm install -S @types/lodash

如果你使用 commonjs 以外的模块选项,则需要将 moduleResolution 选项设置为 node

¥If you’re using a module option other than commonjs, you’ll need to set your moduleResolution option to node.

之后,你将能够毫无问题地导入 lodash,并获得准确的补全。

¥After that, you’ll be able to import lodash with no issues, and get accurate completions.

从模块导出

¥Exporting from Modules

通常,从模块导出涉及将属性添加到 exportsmodule.exports 等值。TypeScript 允许你使用顶层导出语句。例如,如果你像这样导出一个函数:

¥Typically, exporting from a module involves adding properties to a value like exports or module.exports. TypeScript allows you to use top-level export statements. For instance, if you exported a function like so:

js
module.exports.feedPets = function (pets) {
// ...
};

你可以把它写成如下:

¥you could write that out as the following:

ts
export function feedPets(pets) {
// ...
}

有时你会完全覆盖导出对象。这是人们用来使他们的模块立即可调用的常见模式,如以下代码片段所示:

¥Sometimes you’ll entirely overwrite the exports object. This is a common pattern people use to make their modules immediately callable like in this snippet:

js
var express = require("express");
var app = express();

你可能以前这样写过:

¥You might have previously written that like so:

js
function foo() {
// ...
}
module.exports = foo;

在 TypeScript 中,你可以使用 export = 构造对其进行建模。

¥In TypeScript, you can model this with the export = construct.

ts
function foo() {
// ...
}
export = foo;

参数太多/太少

¥Too many/too few arguments

有时你会发现自己调用一个参数太多/太少的函数。通常,这是一个错误,但在某些情况下,你可能已经声明了一个使用 arguments 对象的函数,而不是写出任何参数:

¥You’ll sometimes find yourself calling a function with too many/few arguments. Typically, this is a bug, but in some cases, you might have declared a function that uses the arguments object instead of writing out any parameters:

js
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 的方式。

¥In this case, we need to use TypeScript to tell any of our callers about the ways myCoolFunction can be called using function overloads.

ts
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

¥We added two overload signatures to myCoolFunction. The first checks states that myCoolFunction takes a function (which takes a number), and then a list of numbers. The second one says that it will take a function as well, and then uses a rest parameter (...nums) to state that any number of arguments after that need to be numbers.

顺序添加的属性

¥Sequentially Added Properties

有些人发现创建一个对象并在之后立即添加属性更美观:

¥Some people find it more aesthetically pleasing to create an object and add properties immediately after like so:

js
var options = {};
options.color = "red";
options.volume = 11;

TypeScript 会说你不能分配给 colorvolume,因为它首先将 options 的类型确定为没有任何属性的 {}。如果你改为将声明本身移动到对象字面量中,则不会出现任何错误:

¥TypeScript will say that you can’t assign to color and volume because it first figured out the type of options as {} which doesn’t have any properties. If you instead moved the declarations into the object literal themselves, you’d get no errors:

ts
let options = {
color: "red",
volume: 11,
};

你还可以定义 options 的类型并在对象字面量上添加类型断言。

¥You could also define the type of options and add a type assertion on the object literal.

ts
interface Options {
color: string;
volume: number;
}
let options = {} as Options;
options.color = "red";
options.volume = 11;

或者,你可以只说 options 的类型为 any,这是最容易做的事情,但对你的好处最少。

¥Alternatively, you can just say options has the type any which is the easiest thing to do, but which will benefit you the least.

anyObject{}

¥any, Object, and {}

你可能会想使用 Object{} 来表示一个值可以具有任何属性,因为对于大多数用途而言,Object 是最通用的类型。然而,any 实际上是你在这些情况下想要使用的类型,因为它是最灵活的类型。

¥You might be tempted to use Object or {} to say that a value can have any property on it because Object is, for most purposes, the most general type. However any is actually the type you want to use in those situations, since it’s the most flexible type.

例如,如果你有一些类型为 Object 的内容,你将无法在其上调用 toLowerCase() 之类的方法。更通用通常意味着你可以用一个类型做更少的事情,但 any 的特殊之处在于它是最通用的类型,同时仍然允许你用它做任何事情。这意味着你可以调用它、构建它、访问它的属性等。请记住,无论何时使用 any,你都会失去 TypeScript 为你提供的大部分错误检查和编辑器支持。

¥For instance, if you have something that’s typed as Object you won’t be able to call methods like toLowerCase() on it. Being more general usually means you can do less with a type, but any is special in that it is the most general type while still allowing you to do anything with it. That means you can call it, construct it, access properties on it, etc. Keep in mind though, whenever you use any, you lose out on most of the error checking and editor support that TypeScript gives you.

如果决定归结为 Object{},你应该更喜欢 {}。虽然它们大部分相同,但在某些深奥的情况下,技术上 {}Object 更通用。

¥If a decision ever comes down to Object and {}, you should prefer {}. While they are mostly the same, technically {} is a more general type than Object in certain esoteric cases.

获得更严格的检查

¥Getting Stricter Checks

TypeScript 带有某些检查,可以为你的程序提供更多安全性和分析。将代码库转换为 TypeScript 后,你可以开始启用这些检查以提高安全性。

¥TypeScript comes with certain checks to give you more safety and analysis of your program. Once you’ve converted your codebase to TypeScript, you can start enabling these checks for greater safety.

无隐式 any

¥No Implicit any

在某些情况下,TypeScript 无法确定某些类型应该是什么。为了尽可能宽容,它将决定使用 any 类型来代替它。虽然这对于迁移非常有用,但使用 any 意味着你无法获得任何类型安全,并且你将无法获得与其他地方相同的工具支持。你可以告诉 TypeScript 标记这些位置并使用 noImplicitAny 选项给出错误。

¥There are certain cases where TypeScript can’t figure out what certain types should be. To be as lenient as possible, it will decide to use the type any in its place. While this is great for migration, using any means that you’re not getting any type safety, and you won’t get the same tooling support you’d get elsewhere. You can tell TypeScript to flag these locations down and give an error with the noImplicitAny option.

严格的 nullundefined 检查

¥Strict null & undefined Checks

默认情况下,TypeScript 假定 nullundefined 在每个类型的域中。这意味着用 number 类型声明的任何东西都可以是 nullundefined。由于 nullundefined 是 JavaScript 和 TypeScript 中经常出现错误的来源,因此 TypeScript 提供了 strictNullChecks 选项,让你免于担心这些问题的压力。

¥By default, TypeScript assumes that null and undefined are in the domain of every type. That means anything declared with the type number could be null or undefined. Since null and undefined are such a frequent source of bugs in JavaScript and TypeScript, TypeScript has the strictNullChecks option to spare you the stress of worrying about these issues.

当启用 strictNullChecks 时,nullundefined 获得它们自己的类型,分别称为 nullundefined。只要任何东西可能是 null,你就可以使用具有基础类型的联合类型。因此,例如,如果某物可能是 numbernull,则你会将类型写为 number | null

¥When strictNullChecks is enabled, null and undefined get their own types called null and undefined respectively. Whenever anything is possibly null, you can use a union type with the original type. So for instance, if something could be a number or null, you’d write the type out as number | null.

如果你有一个 TypeScript 认为可能是 null/undefined 的值,但你知道得更多,你可以使用后缀 ! 运算符来告诉它其他情况。

¥If you ever have a value that TypeScript thinks is possibly null/undefined, but you know better, you can use the postfix ! operator to tell it otherwise.

ts
declare var foo: string[] | null;
foo.length; // error - 'foo' is possibly 'null'
foo!.length; // okay - 'foo!' just has type 'string[]'

请注意,使用 strictNullChecks 时,你的依赖可能需要更新才能使用 strictNullChecks

¥As a heads up, when using strictNullChecks, your dependencies may need to be updated to use strictNullChecks as well.

this 没有隐式 any

¥No Implicit any for this

当你在类外使用 this 关键字时,默认情况下它的类型为 any。例如,想象一个 Point 类,想象一个我们希望添加为方法的函数:

¥When you use the this keyword outside of classes, it has the type any by default. For instance, imagine a Point class, and imagine a function that we wish to add as a method:

ts
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);
}
}
// ...
// Reopen the interface.
interface Point {
distanceFromOrigin(): number;
}
Point.prototype.distanceFromOrigin = function () {
return this.getDistance({ x: 0, y: 0 });
};

这与我们上面提到的问题相同 - 我们很容易拼错 getDistance 而不会出现错误。因此,TypeScript 具有 noImplicitThis 选项。设置该选项后,当在没有显式(或推断)类型的情况下使用 this 时,TypeScript 将触发错误。解决方法是使用 this 参数在接口或函数本身中给出显式类型:

¥This has the same problems we mentioned above - we could easily have misspelled getDistance and not gotten an error. For this reason, TypeScript has the noImplicitThis option. When that option is set, TypeScript will issue an error when this is used without an explicit (or inferred) type. The fix is to use a this-parameter to give an explicit type in the interface or in the function itself:

ts
Point.prototype.distanceFromOrigin = function (this: Point) {
return this.getDistance({ x: 0, y: 0 });
};