ts-migrate:Airbnb 進(jìn)行大規(guī)模TypeScript 遷移的神器
這是奶爸碼農(nóng)第71篇原創(chuàng)文章,點(diǎn)擊上方藍(lán)字關(guān)注
引言

業(yè)界對(duì)于TypeScript已經(jīng)是政治正確的選擇了,越來越多的前端庫/框架均采用TS,例如Ant Design 4.0、Vue 3.0等。同時(shí),TS也提供了更健全的語法能力、靜態(tài)的代碼類型檢查、更友好的代碼輔助提示等等。
早在2019年,Airbnb就已經(jīng)在JSConf上分享了其要對(duì)現(xiàn)有前端項(xiàng)目進(jìn)行大規(guī)模TypeScript遷移轉(zhuǎn)換的計(jì)劃。但是對(duì)于現(xiàn)有項(xiàng)目進(jìn)行TypeScript轉(zhuǎn)換并不是一件輕松,Airbnb總共有超過200萬行代碼,超過100個(gè)內(nèi)部npm包。
遷移策略
大規(guī)模代碼遷移往往是一項(xiàng)非常復(fù)雜的工作,Airbnb探索過幾個(gè)遷移策略:
混合遷移策略:一個(gè)個(gè)文件進(jìn)行遷移,修復(fù)類型錯(cuò)誤。allowJS配置可以允許JS和TS文件在一個(gè)項(xiàng)目中并存。使用混合遷移策略可以不用暫停當(dāng)前開發(fā),逐步的一個(gè)個(gè)文件進(jìn)行遷移。但是,對(duì)于研發(fā)人員需要更長的適應(yīng)和遷移時(shí)間。
完全遷移策略:一次性實(shí)現(xiàn)文件完全遷移。我們會(huì)使用any或者@ts-ignore注釋幫助項(xiàng)目編譯,隨后會(huì)補(bǔ)充更加具體的類型申明。
采用完全遷移策略有很多的好處:
項(xiàng)目的一致性:在完全遷移策略下,項(xiàng)目使用使用TypeScript來編寫,因此,開發(fā)者并不需要在JS和TS之間進(jìn)行切換。
修復(fù)類型比修復(fù)文件容易:修復(fù)一個(gè)完整文件非常復(fù)雜,因?yàn)樗赡苡泻芏嗤獠恳蕾嚕虼耍捎没旌线w移的方式,很難保證遷移的進(jìn)度和狀態(tài)。
因此,Airbnb采用了完全遷移策略。然而,一次性遷移完整項(xiàng)目難度非常大。Airbnb研發(fā)了一個(gè)轉(zhuǎn)換工具:ts-migrate,在初試轉(zhuǎn)換過程中,盡可能實(shí)現(xiàn)類型的自動(dòng)轉(zhuǎn)換。
當(dāng)然,這個(gè)工具并不能保證實(shí)現(xiàn)完全沒有錯(cuò)誤的轉(zhuǎn)換,但是在實(shí)際使用過程中,對(duì)于一個(gè)超過50000行代碼、1000個(gè)文件的項(xiàng)目,從JavaScript轉(zhuǎn)換到TypeScript使用這個(gè)工具往往只需要1天!

在Airbnb,React是主要前端框架,所以主要的codemods轉(zhuǎn)換都是基于React概念。ts-migrate也可能可以適用于其他框架或者三方庫。
遷移流程的步驟
讓我們看下一個(gè)JavaScript項(xiàng)目轉(zhuǎn)換到TypeScript所需要的主要步驟。
首先是創(chuàng)建tsconfig.json文件。ts-migrate會(huì)提供一個(gè)默認(rèn)基礎(chǔ)配置文件,下面是個(gè)例子:
{"extends": "../typescript/tsconfig.base.json","include": [".", "../typescript/types"]}
下一步是將代碼后綴從.js/.jsx轉(zhuǎn)換成.ts/.tsx,自動(dòng)化實(shí)現(xiàn)這步其實(shí)非常簡單。
第三步是執(zhí)行codemod,ts-migrate通過plugins方式來組織代碼轉(zhuǎn)換的能力。通過AST解析工具,我們將原有的JS代碼,進(jìn)行解析,分析類型,添加聲明,然后生成相應(yīng)的TS代碼。
codemod可以理解成一種基于AST對(duì)源代碼進(jìn)行修改的方式,如下圖所示,一個(gè)函數(shù)表達(dá)可以通過AST解析成一個(gè)語法樹。

通過如下轉(zhuǎn)換器,可以實(shí)現(xiàn)將所有變量申明進(jìn)行翻轉(zhuǎn):

會(huì)輸出如右圖所示的代碼:

ts-migrate概覽
ts-migrate有三個(gè)部分組成,并且開源在https://github.com/airbnb/ts-migrate
ts-migrate
ts-migrate-server
ts-migrate-plugins
轉(zhuǎn)換工具并不能將一個(gè)項(xiàng)目完全遷移完成,但是,它會(huì)通過ignore的注釋讓編譯器忽略錯(cuò)誤,從而可以讓項(xiàng)目盡可能運(yùn)行起來,隨后你可以再逐步修復(fù)錯(cuò)誤問題。
遷移的整體過程如下:
解析tsconfig.json
創(chuàng)建.ts源代碼文件
把每一個(gè)文件都放到TS Server進(jìn)行診斷,包括三種類型的診斷:semanticDiagnostics(語義診斷),syntacticDiagnostics(句法診斷)和suggestionDiagnostics(建議診斷)。通過這些診斷,工具會(huì)找到需要修改的代碼內(nèi)容,并且進(jìn)行標(biāo)記。
對(duì)每一個(gè)文件執(zhí)行plugin。plugin會(huì)對(duì)源代碼進(jìn)行修改,并且通知TS Server。
通用性Plugin
plugin都會(huì)放在ts-migrate-plugins目錄下面,我們可以先看下兩個(gè)插件:explicitAnyPlugin和declareMissingClassPropertiesPlugin。
exlicitAnyPlugin主要會(huì)對(duì)所有文件中的語義診斷錯(cuò)誤進(jìn)行處理。對(duì)于無法推導(dǎo)類型的變量添加any,可以幫助解決編譯問題。
遷移前:
const fn2 = function(p3, p4) {}const var1 = [];
遷移后:
const fn2 = function(p3: any, p4: any) {}const var1: any = [];
declareMissingClassPropertiesPlugin同樣也會(huì)找到類申明中缺失的類型,并且添加any修飾。
React相關(guān)的插件
reactPropsPlugin可以將PropTypes的類型信息轉(zhuǎn)換成TypeScript的props類型申明。這個(gè)插件會(huì)在.tsx文件中執(zhí)行,reactPropsPlugin會(huì)尋找所有PropTypes的定義,通過AST進(jìn)行解析,并且將其轉(zhuǎn)換成一個(gè)新的props申明:type Props = {...}。
React中的state和生命周期非常常見,我們通過兩個(gè)插件來處理。
如果一個(gè)組件是有狀態(tài)的,reactClassStatePlugin會(huì)生成一個(gè)新的語句:type State = any; reactClassLifecycleMethodsPlugin會(huì)給生命周期方法提供相應(yīng)的類型申明。
整個(gè)工具依舊有很大的改進(jìn)空間,但是,可以作為TypeScript的初始工具,很好的幫你開始整個(gè)遷移過程。這個(gè)工具并不支持hooks,因?yàn)锳irbnb原始項(xiàng)目使用的是老版本的React。
確保項(xiàng)目編譯成功
我們的目標(biāo)是希望將項(xiàng)目進(jìn)行基本類型轉(zhuǎn)換,同時(shí)不改變?nèi)魏未a行為。
通過工具轉(zhuǎn)換可能會(huì)導(dǎo)致代碼lint檢查失敗,因此,可以使用Prettier進(jìn)行代碼自動(dòng)格式化,通過ESLint確保代碼符合規(guī)范。
遷移過程的最后一步是保證所有TypeScript的編譯錯(cuò)誤可以發(fā)現(xiàn)并且修復(fù)。我們會(huì)在這些地方插入@ts-ignore并且提供備注。
// @ts-ignore ts-migrate(7053) FIXME: No index signature with a parameter of type 'string...const { field1, field2, field3 } = DATA[prop];// @ts-ignore ts-migrate(2532) FIXME: Object is possibly 'undefined'.const field2 = object.some_property;
工具也會(huì)支持JSX語法
{*// @ts-ignore ts-migrate(2339) FIXME: Property 'NORMAL' does not exist on type 'typeof W... */}some textid="input"// @ts-ignore ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'string'.name={getName()}/>
有了這些注釋和提示,可以很方便研發(fā)人員進(jìn)行修改。這些注釋,也能夠幫助我們了解代碼的質(zhì)量,并且發(fā)現(xiàn)潛在的代碼問題。
總結(jié)
Airbnb的TypeScript代碼轉(zhuǎn)換還在進(jìn)行中,我們依舊還有一些老舊代碼項(xiàng)目使用JavaScript,我們也有不少$TSFixMe和@ts-ignore注釋需要修復(fù)。

使用ts-migrate工具極大的提升了我們的遷移效率。研發(fā)人員更加關(guān)注于類型的修復(fù)。目前,我們已經(jīng)轉(zhuǎn)換了~86%的代碼,到年底我們預(yù)期會(huì)達(dá)到95%。
- End?-
??看完三件事
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)贊,讓更多的人也能看到介紹內(nèi)容(收藏不點(diǎn)贊,都是耍流氓-_-) 關(guān)注公眾號(hào)“前端勸退師”,不定期分享原創(chuàng)知識(shí)。 也看看其他文章
勸退師個(gè)人微信:huab119
也可以來我的GitHub博客里拿所有文章的源文件:
前端勸退指南:https://github.com/roger-hiro/BlogFN一起玩耍呀
