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
如果您在 src 目錄之外還有一個 tests 資料夾,您可能需要在 src 中配置一個 tsconfig.json,在 tests 中也配置一個。
編寫配置檔案
TypeScript 使用名為 tsconfig.json 的檔案來管理專案的選項,例如您希望包含哪些檔案,以及希望執行何種型別的檢查。讓我們為專案建立一個最基本的配置
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,我們有關於結合 TypeScript 使用 Gulp,以及與 Browserify、Babelify 和 Uglify 等常用構建工具整合的教程。您可以在那裡閱讀更多資訊。
Webpack
Webpack 整合非常簡單。您可以使用 TypeScript 載入器 ts-loader,結合 source-map-loader 以獲得更輕鬆的除錯體驗。只需執行
shellnpm install ts-loader source-map-loader
並將以下選項合併到您的 webpack.config.js 檔案中
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 檔案的任何其他載入器之前執行。
您可以在我們的關於 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/CommonJSdeclare function require(path: string): any;
或者
ts// For RequireJS/AMDdeclare function define(...args: any[]): any;
但更好的做法是去掉這些呼叫,改用 TypeScript 的匯入語法。
首先,您需要透過設定 TypeScript 的 module 選項來啟用某個模組系統。有效選項為 commonjs、amd、system 和 umd。
如果您有以下 Node/CommonJS 程式碼
jsvar foo = require("foo");foo.doStuff();
或者以下 RequireJS/AMD 程式碼
jsdefine(["foo"], function (foo) {foo.doStuff();});
那麼您應該編寫以下 TypeScript 程式碼
tsimport foo = require("foo");foo.doStuff();
獲取宣告檔案
如果您開始轉換為 TypeScript 匯入,很可能會遇到類似 Cannot find module 'foo'. 的錯誤。這裡的問題是您可能沒有用於描述庫的宣告檔案。幸運的是,這非常簡單。如果 TypeScript 對像 lodash 這樣的包有意見,您只需編寫
shellnpm install -S @types/lodash
如果您使用的模組選項不是 commonjs,則需要將 moduleResolution 選項設定為 node。
之後,您將能夠無誤地匯入 lodash,並獲得準確的補全。
從模組匯出
通常,從模組匯出涉及將屬性新增到像 exports 或 module.exports 這樣的值上。TypeScript 允許您使用頂級匯出語句。例如,如果您像這樣匯出一個函式
jsmodule.exports.feedPets = function (pets) {// ...};
您可以將其寫為如下形式
tsexport function feedPets(pets) {// ...}
有時您會完全覆蓋 exports 物件。這是一個常見的模式,人們用它來使他們的模組可以立即被呼叫,如下面的程式碼片段所示
jsvar express = require("express");var app = express();
您可能以前是這樣寫的
jsfunction foo() {// ...}module.exports = foo;
在 TypeScript 中,您可以使用 export = 結構來建模。
tsfunction foo() {// ...}export = foo;
引數過多/過少
有時您會發現自己呼叫函式時引數過多或過少。通常這是一個錯誤,但在某些情況下,您可能聲明瞭一個使用 arguments 物件而不是寫出任何引數的函式
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 可以以何種方式被呼叫。
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 型別。
依次新增的屬性
有些人覺得建立物件並立即在其後新增屬性在美學上更令人愉悅,如下所示
jsvar options = {};options.color = "red";options.volume = 11;
TypeScript 會說您不能賦值給 color 和 volume,因為它最初推斷 options 的型別為 {},而該型別沒有任何屬性。如果您將宣告移入物件字面量本身,就不會有錯誤
tslet options = {color: "red",volume: 11,};
您也可以定義 options 的型別,並對物件字面量進行型別斷言。
tsinterface 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 意味著您沒有得到任何型別安全,也不會得到在其他地方所能獲得的相同工具支援。您可以使用 noImplicitAny 選項告訴 TypeScript 標記這些位置併發出錯誤。
嚴格的 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,但您很確定它不是,可以使用字尾 ! 運算子來告知它。
tsdeclare 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 類,以及一個我們希望新增為方法的函式
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 引數在介面或函式本身中提供一個顯式型別
tsPoint.prototype.distanceFromOrigin = function (this: Point) {return this.getDistance({ x: 0, y: 0 });};