選單

Airbnb 是如何從 JavaScript 遷移到 TypeScript 的?

Airbnb 是如何從 JavaScript 遷移到 TypeScript 的?

作者 | Sergii Rudenko

譯者 | 張健欣

策劃 | 曉旭

TypeScript 是 Airbnb 前端開發的官方語言。但是,採用 TypeScript 的過程和遷移一個包含成千上萬個 JavaScript 檔案的成熟程式碼庫不是一夕發生的。TypeScript 的採用經過了最初提案、多數團隊採用、測試階段,最後落地為 Airbnb 前端開發的官方語言。

遷移策略

大規模遷移是一項複雜的任務,我們探討了從 JavaScript 遷移到 TypeScript 的幾種策略:

1) 混合遷移策略。

一份檔案一份檔案地逐步部分遷移,修復型別錯誤,不斷重複直到整個專案遷移完成。其 allowJS 配置選項允許我們在專案中同時擁有 TypeScript 和 JavaScript 檔案,這使得這種方案變得可行!

在混合遷移策略中,我們不必暫停開發,可以一份檔案一份檔案地逐步遷移。不過,規模很大時,這可能花費很長時間。另外,還需要對來自組織的不同部門的工程師進行培訓。

2) 一次性全部遷移!

將一個 JavaScript 專案或含有部分 TypeScript 的專案完全遷移到 TypeScript。我們需要增加一些 any 型別和 @ts-ignore 註釋,這樣專案編譯就不會報錯,但隨著時間的推移,我們可以用更具描述性的型別替換它們。

選擇一次性全部遷移策略有幾個顯著的優點:

跨專案的一致性:一次性全部遷移將保證每個檔案的狀態相同,工程師不必記住他們可以在哪裡使用 TypeScript 特性,以及編譯器在哪些地方會報錯。

只修復一種型別比修復檔案容易地多:修復整個檔案可能非常複雜,因為檔案可能有許多依賴。使用混合遷移,更難追蹤遷移的實際進度和檔案的狀態。

看起來,一次性全部遷移明顯更好!但是,對一個大而成熟的程式碼庫執行整體遷移的過程是一個重要且複雜的問題。為了解決這個問題,我們決定使用程式碼修改指令碼——codemods!透過我們最初手動遷移到 TypeScript 的過程,我們認識到可以自動化的重複操作。我們為每個步驟製作了 codemods,並將它們組合到總體遷移管線中。

根據我們的經歷,並不能 100% 保證自動化遷移會產生一個完全沒有錯誤的專案,但是我們發現下面列出的步驟的組合為我們最終遷移到一個沒有錯誤的 TypeScript 專案提供了最好的結果。使用 codemods,我們能夠在一天內將包含 50,000 行程式碼和 1,000+ 檔案的專案從 JavaScript 轉換為 TypeScript!

基於這個管線,我們建立了一個稱為“ts-migrate”的工具:

Airbnb 是如何從 JavaScript 遷移到 TypeScript 的?

在 Airbnb,我們在前端程式碼庫的很多重要部分使用了 React。這就是 codemods 的一些部分與基於 React 的概念相關的原因。ts-migrate 可以透過一些額外的配置和測試,與其它框架或庫一起使用。

遷移過程的步驟

讓我們瞭解一下將專案從 JavaScript 遷移到 TypeScript 所需的主要步驟,以及這些步驟是如何實現的:

1) 每個 TypeScript 專案的第一步是建立一個 tsconfig。json 檔案,如果需要,ts-migrate 可以生成這個檔案。有一個預設的配置檔案模板和一個校驗檢查,可以幫助我們確保所有專案的配置是一致的。

下面是一個基本配置的示例:

2) 一旦 tsconfig。json 檔案就位,下一步就是將原始檔的檔案字尾從。js/。jsx 改為。ts/。tsx 。將這一步自動化非常簡單,能夠避免大量人工工作。3)下一步是執行 codemods!我們稱它們為“外掛”。ts-migrate 外掛是可以透過 TypeScript 語言伺服器訪問其他資訊的 codemods。這些外掛以字串作為輸入,產生一個更新後的字串作為輸出。可以使用 jscodeshift、TypeScript API、字串替換或其它 AST 修改工具來進行程式碼轉換。

在每一個步驟之後,我們會檢查 Git 歷史中是否有任何更改並提交它們。這有助於將遷移拉取請求拆分為更易於理解的提交,並跟蹤檔案重新命名。

ts-migrate 包概覽

我們將 ts-migrate 拆分為 3 個包:

ts-migrate

ts-migrate-server

ts-migrate-plugins

這樣做,我們將轉換邏輯從核心執行程式中分離出來,併為不同的目的建立多個配置。目前,我們有兩個主要配置:migration 和 reignore。

雖然 migration 配置的目標是從 JavaScript 遷移到 TypeScript,reignore 的目標是透過忽略所有的錯誤來使得專案可以編譯。當一個人有一個非常大的程式碼庫並且正在執行以下任務時,reignore 是非常有用的:

升級 TypeScript 版本

對程式碼庫進行重大更改或重構

改進一些常用庫的型別

這樣,即使存在一些我們不想立即處理的錯誤,我們也可以遷移專案。這使得 TypeScript 或庫的更新變得容易許多。

這兩個配置都執行在 ts-migrate-server 上,這個 ts-migrate-server 包括兩部分:

TSServer: 這部分與 VSCode 編輯器在編輯器與語言伺服器之間進行通訊時所做的非常相似。TypeScript 語言伺服器的一個新例項作為一個單獨的程序執行,開發工具使用語言協議與伺服器通訊。

Migration runner: 這部分執行並協調遷移過程。它需要以下引數:

它執行以下動作:

解析 tsconfig。json。

建立。ts 原始檔。

將每個檔案傳送到 TypeScript 語言伺服器進行診斷。編譯器為我們提供了三種類型的診斷:語義診斷(semanticDiagnostics )、語法診斷(syntacticDiagnostics )和推理診斷(suggestionDiagnostics )。我們使用這些診斷來發現原始碼中有問題的地方。根據唯一的診斷編號和行號,我們可以確定潛在的問題型別並進行必要的程式碼修改。

在每個檔案上執行所有外掛。如果文字由於外掛的執行而改變,我們就更新原始檔案的內容,並通知 TypeScript 語言伺服器該檔案已經改變。

你可以在 examples package 或 main package 中找到 ts-migrate-server 用法的示例。ts-migrate-example 還包括外掛的基本示例。它們可分為 3 大類:

基於 jscodeshift 的外掛

基於 TypeScript 抽象語法樹的外掛

基於文字的外掛

在程式碼庫中有一組示例演示如何構建各種外掛,並將它們與 ts-migrate-server 結合使用。下面是一個轉換如下程式碼的遷移管線的示例:

ts-migrate 在上面的示例中做了 3 個轉換:

反轉了所有識別符號 first -> tsrif

向函式宣告添加了型別 function tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number

插入 console。log(‘args:$’);

通用外掛

實際的外掛位於單獨的包中——ts-migrate-plugins。我們來看看其中一些外掛。我們有兩個基於 jscodeshift 的外掛:explicitAnyPlugin 和 declareMissingClassPropertiesPlugin。jscodeshift 是一個使用 recast 包將抽象語法樹(AST)轉換回字串的工具。透過使用 toSource() 函式,我們可以直接更新檔案的原始碼。

explicitAnyPlugin 背後的主要思想是從 TypeScript 語言伺服器中提取所有語義診斷錯誤以及行號。然後,我們需要在診斷中指定的行上新增 any 型別。這種方法允許我們解決錯誤,因為新增 any 型別可以修復編譯錯誤。

轉換前:

轉換後:

declareMissingClassPropertiesPlugin 接受所有程式碼為 2339(你能猜出這個程式碼是什麼意思嗎?)的診斷,如果它能找到缺失識別符號的類宣告,這個外掛會使用 any 型別註解將它們新增到類主體中。從名字可以看出,這個 codemod 只適用於 ES6 類。

下一類外掛是基於 TypeScript AST 的外掛。透過解析 AST,我們可以在原始檔中生成具有如下型別的更新陣列:

在生成更新後,剩下的唯一事情就是以相反的順序應用這些更改。如果透過這些操作的結果,我們接收到新的文字,我們就更新原始檔。讓我們來看看這些基於 AST 的外掛:stripTSIgnorePlugin 和 hoistClassStaticsPlugin。

stripTSIgnorePlugin 是遷移管線中的第一個外掛。它從檔案中刪除所有 @ts-ignore(@ts-ignore 註釋允許我們告訴編譯器忽略下一行中的錯誤)例項。如果我們正在將一個 JavaScript 專案轉換成 TypeScript,這個外掛不會做任何事情。但是,如果這是一個有一部分 TypeScript 的專案(在 Airbnb,我們有一些處於這種狀態的專案),那麼這是必不可少的第一步。只有在刪除 @ts-ignore 註釋後,TypeScript 編譯器才會發出所有需要解決的診斷錯誤。

轉換為:

在刪除 @ts-ignore 註釋後,我們執行 hoistClassStaticsPlugin。這個外掛遍歷檔案中的所有類宣告。它決定我們是否可以提升識別符號或表示式,並確定是否已經將賦值提升到類。

為了能夠快速迭代並防止迴歸,我們為每個外掛和 ts-migrate 增加了一系列單元測試。

React 相關外掛

reactPropsPlugin 將型別資訊從 PropTypes 轉換為一個 TypeScript 屬性型別定義。這個外掛是基於 Mohsen Azimi 編寫的非常棒的工具。我們只需要在包含至少一個 React 元件的。tsx 檔案上執行這個外掛。reactPropsPlugin 查詢所有 PropTypes 宣告,並嘗試用 AST 和簡單正則表示式(如 /number/)或更復雜的正則表示式(如 /objectOf$/)來解析它們。當檢測到一個 React 元件(無論是函式式元件還是類元件),它將被轉換為一個具有新的 type Props = {…}; 屬性型別的元件。

reactDefaultPropsPlugin 覆蓋了 React 元件的 defaultProps 模式。我們使用一種特殊型別來表示具有預設值的 props:

我們試圖找到預設的 props 宣告,並將它們與上一步生成的元件 props 型別合併。

狀態和生命週期的概念在 React 生態系統中很常見。我們在兩個外掛中解決了它們。如果一個元件是有狀態的,reactClassStatePlugin 生成一個新的 type State = any; ,reactClassLifecycleMethodsPlugin 用適當的型別註解元件的生命週期方法。這些外掛的功能可以擴充套件,包括用更具描述性的型別替換 any 的能力。

對狀態和 props 的型別支援有更多改進的空間。然而,作為一個起點,這個功能被證明是足夠的。我們還不涉及 hooks,因為一開始遷移的時候,我們的程式碼庫使用的是比較老的 React 版本。

確保專案編譯成功

我們的目標是獲得一個可編譯的 TypeScript 專案,它的基本型別覆蓋不會導致應用程式執行時行為的改變。

在進行所有轉換和程式碼修改之後,我們的程式碼可能會有不一致的格式,並且一些 lint 檢查可能會失敗。我們的前端程式碼庫依賴一個 prettier-eslint 設定——Prettier 用來自動格式化程式碼,ESLint 確保程式碼遵循最佳實踐。因此,我們可以透過從我們的外掛執行 eslint-prettier 來快速修復前面步驟可能引入的任何格式問題。

遷移管線的最後一部分確保所有的 TypeScript 編譯衝突都得到解決。為了檢測和修復潛在的錯誤,tsIgnorePlugin 使用行號進行語義診斷,並插入帶有有用解釋的 @ts-ignore 註釋,例如:

我們也增加了對 JSX 語法的支援:

在註釋中包含有意義的錯誤資訊可以更容易地修復問題和重新訪問需要注意的程式碼。這些註釋,結合 $TSFixMe (我們為 any 型別引入了自定義的別名 $TSFixMe 和函式型別——$TSFixMeFunction = (…args: any[]) => any; 。儘管最佳實踐是避免使用 any 型別,但使用它可以幫助我們簡化遷移過程,並明確哪些型別應該重新訪問),使得我們可以收集有關程式碼質量的有用資料,並確定可能存在問題的程式碼區域。

最後值得一提的是,我們需要執行 eslint-fix 外掛兩次。一次是在 tsIgnorePlugin 之前,給定的格式可能會影響我們在哪裡得到編譯錯誤。另一次是在 tsIgnorePlugin 之後,因為插入 @ts-ignore 註釋可能會引入新的格式錯誤。

總結

我們的遷移故事正在進行中:我們有一些遺留專案仍然在用 JavaScript,我們在程式碼庫中仍然有大量的 $TSFixMe 和 @ts-ignore 註釋。

Airbnb 是如何從 JavaScript 遷移到 TypeScript 的?

但是,使用 ts-migrate 大大加快了我們遷移的過程和效率。工程師們能夠專注於型別改進,而不是手動進行逐檔案的遷移。目前,我們的 600 萬行前端程式碼庫的大約 86% 已經轉換為 TypeScript,到今年年底,我們有望達到 95%。

你可以檢出 ts-migrate 程式碼,並在 GitHub 程式碼庫的主包中找到如何安裝和執行 ts-migrate 的說明。如果你發現了任何問題或者有任何改進的想法,我們歡迎你的貢獻!

Brie Bunge 是 Airbnb TypeScript 的幕後推動者,也是 ts-migrate 的建立者,對其致以最大的敬意。感謝 Joe Lencioni 幫助我們在 Airbnb 採用 TypeScript,並改進我們的 TypeScript 基礎設施和工具。特別感謝 Elliot Sachs 和 John Haytko 對 ts-migrate 所做的貢獻。感謝所有一路提供反饋和幫助的人!

後記

我們在遷移過程中發現的一些有用的東西:

TypeScript 的 3。7 版本引入了 @ts-nocheck 註釋,可以增加在 TypeScript 檔案的頭部來禁用語義檢查。我們沒有使用這個註釋,因為它之前不支援。ts/。tsx 檔案,但它也可以在遷移過程中成為一個很好的中間階段助手。

TypeScript 的 3。9 版本引入了 @ts-expect-error 註釋。當一行以 @ts-expect-error 註釋作為字首時,TypeScript 將禁止報告該錯誤。如果沒有錯誤,TypeScript 會報告 @ts-expect-error 是不必要的。在 Airbnb 程式碼庫,我們使用了 @ts-expect-error 而不是 @ts-ignore 。

https://medium。com/airbnb-engineering/ts-migrate-a-tool-for-migrating-to-typescript-at-scale-cd23bfeb5cc