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,我們有一個關於 使用 Gulp 搭配 TypeScript,並與常見的建置工具(例如 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
。雖然這可能會讓人有點不知所措,但長期收益會顯得更快。
清除錯誤
如我們所提到的,在轉換後收到錯誤訊息並非意外。重要的是要逐一檢視這些錯誤,並決定如何處理它們。這些錯誤通常是合法的錯誤,但有時您必須向 TypeScript 更好地解釋您想做什麼。
從模組匯入
你可能會開始收到一堆錯誤,例如 找不到名稱 'require'。
和 找不到名稱 '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 程式碼
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 匯入,你可能會遇到 找不到模組 'foo'。
之類的錯誤。這裡的問題是你可能沒有宣告檔案來描述你的程式庫。幸運的是,這很容易。如果 TypeScript 抱怨像 lodash
這樣的套件,你可以寫
shell
npm install -S @types/lodash
如果你使用的是 commonjs
以外的模組選項,你需要將你的 moduleResolution
選項設定為 node
。
在那之後,你將能夠毫無問題地匯入 lodash,並獲得準確的完成。
從模組匯出
通常,從模組匯出包含將屬性新增至值,例如 exports
或 module.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
清單。第二個表示它也會採用一個函式,然後使用一個 rest 參數 (...nums
) 來表示該參數之後的任何數量的參數都需要是 number
。
順序新增的屬性
有些人認為這樣建立物件並立即新增屬性,在美觀上比較令人滿意
js
var options = {};options.color = "red";options.volume = 11;
TypeScript 會說你無法指定 color
和 volume
,因為它首先將 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
,這是最簡單的方法,但對你的幫助最小。
any
、Object
和 {}
你可能會想使用 Object
或 {}
來表示一個值可以擁有任何屬性,因為 Object
在大多數情況下是最通用的類型。然而,any
才是你實際上想在這種情況下使用的類型,因為它是最靈活的類型。
例如,如果你有一個類型為 Object
的東西,你將無法對它呼叫像 toLowerCase()
這樣的函式。通常,更通用表示你可以使用類型做的事情更少,但 any
很特別,因為它是最通用的類型,同時仍然允許你對它做任何事情。這表示你可以呼叫它、建構它、存取它的屬性,等等。不過請記住,每當你使用 any
時,你都會失去 TypeScript 提供給你的大部分錯誤檢查和編輯器支援。
如果一個決定歸結為 Object
和 {}
,你應該優先選擇 {}
。雖然它們大部分相同,但技術上在某些深奧的情況下,{}
是比 Object
更通用的類型。
取得更嚴格的檢查
TypeScript 附帶某些檢查,可提供更安全的程式和分析。一旦將程式碼庫轉換為 TypeScript,就可以開始啟用這些檢查以獲得更高的安全性。
沒有隱含的 any
在某些情況下,TypeScript 無法找出某些類型應該是什麼。為了盡可能寬鬆,它會決定使用類型 any
來代替。雖然這對於遷移來說很好,但使用 any
表示您沒有獲得任何類型安全性,而且您不會獲得在其他地方獲得的相同工具支援。您可以告訴 TypeScript 標記這些位置並使用 noImplicitAny
選項給出錯誤。
嚴格的 null
和 undefined
檢查
預設情況下,TypeScript 假設 null
和 undefined
在每個類型的網域中。這表示宣告為類型 number
的任何東西都可能是 null
或 undefined
。由於 null
和 undefined
是 JavaScript 和 TypeScript 中錯誤的常見來源,因此 TypeScript 有 strictNullChecks
選項,讓您不必擔心這些問題。
當 strictNullChecks
已啟用時,null
和 undefined
會取得自己的類型,分別稱為 null
和 undefined
。每當任何東西可能為 null
時,您可以使用與原始類型結合的聯合類型。因此,例如,如果某個東西可能是 number
或 null
,您會將類型寫成 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
選項。當設定該選項時,TypeScript 會在未明確 (或推斷) 類型的情況下使用 this
時發出錯誤。解決方法是在介面或函式本身中使用 this
參數來提供明確類型
ts
Point.prototype.distanceFromOrigin = function (this: Point) {return this.getDistance({ x: 0, y: 0 });};