从 JavaScript 迁移

TypeScript 并非凭空产生。它的构建充分考虑了 JavaScript 生态系统,而且目前已经存在大量的 JavaScript 代码。将 JavaScript 代码库转换为 TypeScript 虽然有些繁琐,但通常并不困难。在本教程中,我们将探讨如何开始这一过程。我们假设您已经阅读了足够的手册内容,可以编写新的 TypeScript 代码。

如果您打算转换一个 React 项目,我们建议您先查看 React 转换指南

设置您的目录

如果您正在编写纯 JavaScript,很可能您是直接运行 JavaScript 的,即您的 .js 文件位于 srclibdist 目录中,然后按需运行。

如果是这种情况,您编写的文件将作为 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 指定了几件事

  1. 读取 src 目录中它能理解的所有文件(使用 include)。
  2. 接受 JavaScript 文件作为输入(使用 allowJs)。
  3. 将所有输出文件发送到 built 目录(使用 outDir)。
  4. 将较新的 JavaScript 结构转换(降级)到较旧的版本,如 ECMAScript 5(使用 target)。

此时,如果您尝试在项目根目录下运行 tsc,应该会在 built 目录中看到输出文件。built 中的文件布局应与 src 中的布局完全一致。现在 TypeScript 应该已经可以在您的项目中工作了。

早期收益

即使是在现阶段,您也能从 TypeScript 对项目的理解中获得巨大收益。如果您打开像 VS CodeVisual Studio 这样的编辑器,您会发现通常可以获得一些工具支持,如自动补全。您还可以通过以下选项捕获特定的错误:

TypeScript 还会针对无法访问的代码和标签发出警告,您可以分别通过 allowUnreachableCodeallowUnusedLabels 来禁用这些警告。

集成构建工具

您的流水线中可能还有更多的构建步骤。也许您需要将某些内容拼接到每个文件中。每个构建工具都不同,但我们将尽力涵盖其要点。

Gulp

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

Webpack

Webpack 集成非常简单。您可以使用 TypeScript 加载器 ts-loader,结合 source-map-loader 以获得更轻松的调试体验。只需运行

shell
npm install ts-loader source-map-loader

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

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 文件的任何其他加载器之前运行。

您可以在我们的关于 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/CommonJS
declare function require(path: string): any;

或者

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

但更好的做法是去掉这些调用,改用 TypeScript 的导入语法。

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

如果您有以下 Node/CommonJS 代码

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

或者以下 RequireJS/AMD 代码

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

那么您应该编写以下 TypeScript 代码

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

获取声明文件

如果您开始转换为 TypeScript 导入,很可能会遇到类似 Cannot find module 'foo'. 的错误。这里的问题是您可能没有用于描述库的声明文件。幸运的是,这非常简单。如果 TypeScript 对像 lodash 这样的包有意见,您只需编写

shell
npm install -S @types/lodash

如果您使用的模块选项不是 commonjs,则需要将 moduleResolution 选项设置为 node

之后,您将能够无误地导入 lodash,并获得准确的补全。

从模块导出

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

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

您可以将其写为如下形式

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

有时您会完全覆盖 exports 对象。这是一个常见的模式,人们用它来使他们的模块可以立即被调用,如下面的代码片段所示

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

您可能以前是这样写的

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

在 TypeScript 中,您可以使用 export = 结构来建模。

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

参数过多/过少

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

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 可以以何种方式被调用。

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 类型。

依次添加的属性

有些人觉得创建对象并立即在其后添加属性在美学上更令人愉悦,如下所示

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

TypeScript 会说您不能赋值给 colorvolume,因为它最初推断 options 的类型为 {},而该类型没有任何属性。如果您将声明移入对象字面量本身,就不会有错误

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

您也可以定义 options 的类型,并对对象字面量进行类型断言。

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

或者,您可以直接说 options 的类型是 any,这是最容易做到的,但收益最少。

anyObject{}

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

例如,如果某个东西的类型被设为 Object,您将无法对其调用像 toLowerCase() 这样的方法。更通用通常意味着您对该类型能做的事情更少,但 any 很特别,它是最通用的类型,同时又允许您对其进行任何操作。这意味着您可以调用它、构造它、访问其属性等。但请记住,每当您使用 any 时,您都会失去 TypeScript 提供的绝大部分错误检查和编辑器支持。

如果需要在 Object{} 之间做出决定,您应该优先选择 {}。虽然它们在大多数情况下是相同的,但从技术上讲,在某些深奥的情况下,{} 是比 Object 更通用的类型。

获取更严格的检查

TypeScript 附带了某些检查,旨在为您提供更高的安全性和程序分析。一旦您将代码库转换为 TypeScript,就可以开始启用这些检查以获得更高的安全性。

禁止隐式 any

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

严格的 nullundefined 检查

默认情况下,TypeScript 假设 nullundefined 属于每种类型的域。这意味着任何声明为 number 类型的变量都可能是 nullundefined。由于 nullundefined 是 JavaScript 和 TypeScript 中 Bug 的常见来源,TypeScript 提供了 strictNullChecks 选项,让您免于担心这些问题。

当启用 strictNullChecks 时,nullundefined 分别拥有自己的类型,即 nullundefined。每当某项内容可能null 时,您可以使用原始类型的联合类型。例如,如果某项内容可能是 numbernull,您可以将其类型写为 number | null

如果您有一个值,TypeScript 认为它可能为 null/undefined,但您很确定它不是,可以使用后缀 ! 操作符来告知它。

ts
declare 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 类,以及一个我们希望添加为方法的函数

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 参数在接口或函数本身中提供一个显式类型

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

TypeScript 文档是一个开源项目。通过发送 Pull Request 来帮助我们改进这些页面 ❤

此页面的贡献者
DRDaniel Rosenwasser (57)
OTOrta Therox (14)
TAThomas Ankcorn (3)
MGMaayan Glikser (3)
MFMartin Fischer (1)
19+

最后更新:2026 年 3 月 27 日