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 文件位于 src、lib 或 dist 目录下,然后按需要运行。
🌐 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:
- 读取
src目录中它能识别的任何文件(使用include)。 - 接受 JavaScript 文件作为输入(使用
allowJs)。 - 将所有输出文件生成到
built(使用outDir)。 - 将较新的 JavaScript 构造转换为较旧的版本,例如 ECMAScript 5(使用
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 Code 或 Visual 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:
noImplicitReturns可以防止你忘记在函数结束时返回。noFallthroughCasesInSwitch如果你不想忘记在switch块中的case之间的break语句,这将非常有用。
TypeScript 还会警告不可达的代码和标签,你可以分别通过 allowUnreachableCode 和 allowUnusedLabels 来禁用它们。
🌐 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
shellnpm install ts-loader source-map-loader
并将以下选项合并到你的 webpack.config.js 文件中:
🌐 and merge in options from the following into your webpack.config.js file:
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 文件的任何其他加载器之前运行。
🌐 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/CommonJSdeclare function require(path: string): any;
or
ts// For RequireJS/AMDdeclare function define(...args: any[]): any;
最好摆脱这些调用并使用 TypeScript 语法进行导入。
🌐 it’s better to get rid of those calls and use TypeScript syntax for imports.
首先,你需要通过设置 TypeScript 的 module 选项来启用某些模块系统。有效的选项有 commonjs、amd、system 和 umd。
🌐 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:
jsvar foo = require("foo");foo.doStuff();
或以下 RequireJS/AMD 代码:
🌐 or the following RequireJS/AMD code:
jsdefine(["foo"], function (foo) {foo.doStuff();});
那么你将编写以下 TypeScript 代码:
🌐 then you would write the following TypeScript code:
tsimport 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
shellnpm 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
通常,从模块导出涉及向像 exports 或 module.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:
jsmodule.exports.feedPets = function (pets) {// ...};
你可以把它写成如下:
🌐 you could write that out as the following:
tsexport function feedPets(pets) {// ...}
有时候你会完全覆盖 exports 对象。这是人们常用的一种模式,用来让他们的模块像下面这个示例一样可以立即调用:
🌐 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:
jsvar express = require("express");var app = express();
你可能以前这样写过:
🌐 You might have previously written that like so:
jsfunction foo() {// ...}module.exports = foo;
在 TypeScript 中,你可以使用 export = 构造来建模这个。
🌐 In TypeScript, you can model this with the export = construct.
tsfunction 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:
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 可以有哪些调用方式。
🌐 In this case, we need to use TypeScript to tell any of our callers about the ways myCoolFunction can be called using function overloads.
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。
🌐 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:
jsvar options = {};options.color = "red";options.volume = 11;
TypeScript 会提示你不能给 color 和 volume 赋值,因为它首先将 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:
tslet options = {color: "red",volume: 11,};
你也可以定义 options 的类型,并在对象字面量上添加类型断言。
🌐 You could also define the type of options and add a type assertion on the object literal.
tsinterface 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.
any、Object 和 {}
🌐 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 意味着你无法获得任何类型安全,也无法获得其他地方同样的工具支持。
你可以通过 noImplicitAny 选项告诉 TypeScript 标记这些位置并报错。
🌐 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.
严格的 null 和 undefined 检查
🌐 Strict null & undefined Checks
默认情况下,TypeScript 假设 null 和 undefined 属于每种类型的域。这意味着任何声明为 number 类型的值都可能是 null 或 undefined。由于 null 和 undefined 在 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 时,null 和 undefined 会分别拥有自己的类型,称为 null 和 undefined。
每当某个东西 可能 是 null 时,你可以使用与原类型的联合类型。
例如,如果某个东西可能是 number 或 null,你可以将类型写为 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.
tsdeclare 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:
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 参数来提供显式类型:
🌐 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:
tsPoint.prototype.distanceFromOrigin = function (this: Point) {return this.getDistance({ x: 0, y: 0 });};