從 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 日