TypeScript 與 JavaScript:您的首選指南
已發表: 2022-03-11打字稿還是 JavaScript? 開發人員考慮將這種選擇用於新建 Web 或 Node.js 項目,但對於現有項目來說,這也是一個值得考慮的問題。 作為 JavaScript 的超集,TypeScript 提供了 JavaScript 的所有功能以及一些額外的好處。 TypeScript 本質上鼓勵我們編寫乾淨的代碼,使代碼更具可擴展性。 但是,項目可以包含盡可能多的純 JavaScript,因此使用 TypeScript 並不是一個全有或全無的命題。
TypeScript 和 JavaScript 的關係
TypeScript 為 JavaScript 添加了顯式類型系統,允許嚴格執行變量類型。 TypeScript 在轉譯時運行其類型檢查——一種將 TypeScript 代碼轉換為 Web 瀏覽器和 Node.js 理解的 JavaScript 代碼的編譯形式。
TypeScript 與 JavaScript 示例
讓我們從一個有效的 JavaScript 片段開始:
let var1 = "Hello"; var1 = 10; console.log(var1); 在這裡, var1以string開始,然後變為number 。
由於 JavaScript 只是鬆散類型,我們可以隨時將var1重新定義為任何類型的變量——從字符串到函數。
執行此代碼輸出10 。
現在,讓我們將此代碼更改為 TypeScript:
let var1: string = "Hello"; var1 = 10; console.log(var1); 在這種情況下,我們將var1聲明為string 。 然後我們嘗試為它分配一個數字,這是 TypeScript 嚴格的類型系統所不允許的。 轉譯會導致錯誤:
TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10; 如果我們指示轉譯器將原始 JavaScript 片段視為 TypeScript,轉譯器會自動推斷var1應該是一個string | number string | number 。 這是一個 TypeScript聯合類型,它允許我們隨時為var1分配一個string或一個number 。 解決了類型沖突後,我們的 TypeScript 代碼將成功轉換。 執行它會產生與 JavaScript 示例相同的結果。
30,000 英尺高的 TypeScript 與 JavaScript:可擴展性挑戰
JavaScript 無處不在,為各種規模的項目提供動力,其應用方式在 1990 年代初期是無法想像的。 雖然 JavaScript 已經成熟,但在可擴展性支持方面還不夠。 因此,開發人員努力應對規模和復雜性都在增長的 JavaScript 應用程序。
值得慶幸的是,TypeScript 解決了擴展 JavaScript 項目的許多問題。 我們將專注於前三個挑戰:驗證、重構和文檔。
驗證
我們依靠集成開發環境 (IDE) 來幫助完成添加、修改和測試新代碼等任務,但 IDE 無法驗證純 JavaScript 引用。 我們通過在編碼時警惕地監控來緩解這個缺點,以避免變量和函數名稱中出現拼寫錯誤的可能性。
當代碼來自第三方時,問題的嚴重性呈指數級增長,很少執行的代碼分支中的損壞引用很容易未被檢測到。
相比之下,使用 TypeScript,我們可以將精力集中在編碼上,確信任何錯誤都會在編譯時被識別出來。 為了證明這一點,讓我們從一些遺留的 JavaScript 代碼開始:
const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } } .toISO()調用是 moment.js toISOString()方法的錯字,但如果format參數不是ISO ,代碼將起作用。 當我們第一次嘗試將ISO傳遞給函數時,它會引發這個運行時錯誤: TypeError: moment(...).toISO is not a function 。
定位拼寫錯誤的代碼可能很困難。 當前代碼庫可能沒有通往斷線的路徑,在這種情況下,我們的損壞.toISO()引用不會被測試捕獲。
如果我們將此代碼移植到 TypeScript,IDE 將突出顯示損壞的引用,提示我們進行更正。 如果我們什麼都不做並嘗試轉譯,我們將被阻止,轉譯器將產生以下錯誤:
TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());重構
雖然第三方代碼引用中的拼寫錯誤並不少見,但內部引用中的拼寫錯誤存在一組不同的問題,例如:
const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); } 唯一的開發者可以很容易地找到並修復所有以er結尾的phoneNumbr實例。
但是團隊越大,這個簡單、常見的錯誤就越會造成不合理的代價。 在執行工作的過程中,同事需要注意並傳播此類拼寫錯誤。 或者,添加支持兩種拼寫的代碼會使代碼庫不必要地膨脹。
使用 TypeScript,當我們修復拼寫錯誤時,相關代碼將不再編譯,通知同事將修復傳播到他們的代碼。
文檔
準確和相關的文檔是開發人員團隊內部和之間溝通的關鍵。 JavaScript 開發人員經常使用 JSDoc 來記錄預期的方法和屬性類型。
TypeScript 的語言特性(例如,抽像類、接口和類型定義)促進了按合同設計的編程,從而產生了高質量的文檔。 此外,對對象必須遵循的方法和屬性進行正式定義有助於識別重大更改、創建測試、執行代碼自省和實現架構模式。
對於 TypeScript,首選工具 TypeDoc(基於 TSDoc 提議)自動從我們的代碼中提取類型信息(例如,類、接口、方法和屬性)。 因此,我們毫不費力地創建了迄今為止比 JSDoc 更全面的文檔。
TypeScript 與 JavaScript 的優勢
現在,讓我們探索如何使用 TypeScript 來解決這些可伸縮性挑戰。
高級代碼/重構建議
許多 IDE 可以處理來自 TypeScript 類型系統的信息,在我們編碼時提供參考驗證。 更好的是,當我們鍵入時,IDE 可以提供相關的、一目了然的文檔(例如,函數期望的參數)以供任何參考,並建議上下文正確的變量名稱。
在這個 TypeScript 片段中,IDE 建議自動完成函數返回值中的鍵名:
/** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }當我開始調用函數(第 31 行)時,我的 IDE Visual Studio Code 提供了這個建議(在標註中):
更重要的是,IDE 的自動完成建議(在標註中)在上下文中是正確的,僅顯示嵌套鍵情況中的有效名稱(第 34 行):
這種實時建議可以加快編碼速度。 此外,IDE 可以依靠 TypeScript 嚴格的類型信息來重構任何規模的代碼。 當我們對引用的準確性有 100% 的信心時,諸如重命名屬性、更改文件位置甚至提取超類之類的操作變得微不足道。
接口支持
與 JavaScript 相比,TypeScript 提供了使用接口定義類型的能力。 接口正式列出(但不實現)對象必須包含的方法和屬性。 這種語言結構對於與其他開發人員的協作特別有幫助。
下面的例子強調了我們如何利用 TypeScript 的特性來巧妙地實現常見的 OOP 模式——在這種情況下是策略和責任鏈——從而改進前面的例子:
export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }ES6 模塊——任何地方
在撰寫本文時,並非所有前端和後端 JavaScript 運行時都支持 ES6 模塊。 然而,使用 TypeScript,我們可以使用 ES6 模塊語法:

import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c'])); 轉譯後的輸出將與我們選擇的環境兼容。 例如,使用編譯器選項--module CommonJS ,我們得到:
"use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; 改用--module UMD ,TypeScript 輸出更詳細的 UMD 模式:
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });ES6 類——任何地方
遺留環境通常缺乏對 ES6 類的支持。 TypeScript 轉譯通過使用特定於目標的構造來確保兼容性。 這是一個 TypeScript 源代碼片段:
export class TestClass { hello = 'World'; }JavaScript 輸出取決於模塊和目標,TypeScript 允許我們指定。
這是--module CommonJS --target es3產生的結果:
"use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass; 使用--module CommonJS --target es6代替,我們得到以下轉譯結果。 class關鍵字用於針對 ES6:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;異步/等待功能——任何地方
Async/await 使異步 JavaScript 代碼更易於理解和維護。 TypeScript 為所有運行時提供此功能,即使是那些不提供本地異步/等待的運行時。
請注意,要在 ES3 和 ES5 等較舊的運行時上運行 async/await,您需要對基於Promise的輸出的外部支持(例如,通過 Bluebird 或 ES2015 polyfill)。 TypeScript 附帶的Promise polyfill 很容易集成到轉譯輸出中——我們只需要相應地配置lib編譯器選項。
支持私有類字段——任何地方
即使對於遺留目標,TypeScript 也以與強類型語言(例如,Java 或 C#)相同的方式支持private字段。 相比之下,很多 JavaScript 運行時通過哈希前綴語法支持private字段,這是 ES2022 的一個完成提案。
TypeScript 與 JavaScript 的缺點
現在我們已經強調了實現 TypeScript 的主要好處,讓我們探索 TypeScript 可能不適合的場景。
轉譯:工作流程不兼容的可能性
特定的工作流程或項目要求可能與 TypeScript 的轉譯步驟不兼容:例如,如果我們需要在部署後使用外部工具來更改代碼,或者生成的輸出必須對開發人員友好。
例如,我最近為 Node.js 環境編寫了一個 AWS Lambda 函數。 TypeScript 不適合,因為需要轉譯會阻止我和其他團隊成員使用 AWS 在線編輯器編輯函數。 這對項目經理來說是一個交易破壞者。
類型系統僅在轉換時間之前有效
TypeScript 的 JavaScript 輸出不包含類型信息,因此它不會執行類型檢查,因此類型安全可能會在運行時中斷。 例如,假設一個函數被定義為總是返回一個對象。 如果在.js文件中使用它返回null ,則會發生運行時錯誤。
依賴於類型信息的特性(例如,私有字段、接口或泛型)可以為任何項目增加價值,但在編譯時會被刪除。 例如, private類成員在轉譯後將不再是私有的。 需要明確的是,這種性質的運行時問題並不是 TypeScript 獨有的,你也可以預料到 JavaScript 也會遇到同樣的困難。
結合 TypeScript 和 JavaScript
儘管 TypeScript 有很多好處,但有時我們無法證明一次轉換整個 JavaScript 項目是合理的。 幸運的是,我們可以逐個文件地向 TypeScript 轉譯器指定要解釋為純 JavaScript 的內容。 事實上,這種混合方法有助於緩解項目生命週期中出現的個別挑戰。
如果以下代碼,我們可能更願意保持 JavaScript 不變:
- 由一位前同事編寫,需要大量的逆向工程工作才能轉換為 TypeScript。
- 使用 TypeScript 中不允許的技術(例如,在對象實例化後添加屬性),並且需要重構以遵守 TypeScript 規則。
- 屬於另一個繼續使用 JavaScript 的團隊。
在這種情況下,聲明文件( .d.ts文件,有時稱為定義文件或類型文件)為 TypeScript 提供了足夠的類型數據來啟用 IDE 建議,同時保持 JavaScript 代碼不變。
許多 JavaScript 庫(例如 Lodash、Jest 和 React)在單獨的類型包中提供 TypeScript 類型文件,而其他庫(例如 Moment.js、Axios 和 Luxon)將類型文件集成到主包中。
TypeScript 與 JavaScript:精簡和可擴展性的問題
TypeScript 提供的無與倫比的支持、靈活性和增強功能顯著改善了開發人員體驗,使項目和團隊能夠擴展。 將 TypeScript 整合到項目中的主要成本是增加了轉譯構建步驟。 對於大多數應用程序來說,轉譯成 JavaScript 不是問題。 相反,它是 TypeScript 眾多好處的墊腳石。
進一步閱讀 Toptal 工程博客:
- 使用 TypeScript 和 Jest 支持:AWS SAM 教程
