TypeScript:從嘗試到暫緩

陳乙山
陳乙山
Sep 3 · 10 min read
Image for post
Image for post

最近我們 iCHEF 前端團隊嘗試導入 TypeScript,但最後暫緩了。

背景

一直以來團隊都有嘗試 TypeScript 的想法。除了靜態型別可以在編譯期抓出錯誤、減少 bug 產生之外,更好的開發者體驗(autocompletehover information)也是一個吸引我們的地方。隨著專案 size 增加,前端團隊的人手也逐漸增加到現在的 5 位,上述的好處似乎越來越值得一試,因此我們決定先在我們開源的 React component library gypcrete 嘗試轉移到 TypeScript。

TypeScript 為了 JavaScript 的開發者,已經盡可能減少 migrate 的難度 — TypeScript 專案可以和 .js 檔案混用,也有官方文件教你如何 migrate from JavaScript。然而實際做下去卻發現不少問題,讓我們意識到轉移到 TypeScript 並不如想像的簡單。

以下的發現皆以 TypeScript 3.9 為基準,最近 TypeScript 4.0 剛出爐,也許改善了一些問題,也許沒有,如果有什麼新發現,也歡迎留言。

開發工具

要從既有的 JavaScript 專案轉移到 TypeScript,在建置上並不算太難。babel 7 已經可以用 babel-preset-typescript 解析 TypeScript 並生出 .js 檔案。因此我們只需繼續沿用 babel 的設定,加上 babel-preset-typescript,並且另外用 tsc(TypeScript Compiler) 做型別檢查(babel 本身會忽略型別資訊),就可以達成讓建置過程吃 .ts 、做型別檢查的目的。

然而開發上的問題就比較多一些。我們發現,Typescript 的 linter,還有很多不太方便的地方。

目前 Typescript linter 有三種選擇:

  • tslint: 曾經是 TypeScript linter 的唯一選擇,但現在已經 deprecated 了。因為 tslint 的實作會在 Typescript 的 ast 上實作檢查規則,而 eslint 有自己的 ast 實作(estree),導致許多 eslint 的規則無法簡單轉移到 tslint 上。
  • babel-eslint(@babel/eslint-parser): babel-eslint 則是 eslint 的 parser 之一。背後用 babel 取得 ast,再轉換成 estree。由於 babel 可以接收 TypeScript,就能作為 Typescript 的 linter。然而因為 babel 看不懂型別資訊,有一些誤判的 issue(例如把型別判斷成未定義的變數),而且看起來短時間不會修好:

I don’t think we should consider this a blocker for v8, since TS support is currently fairly limited. I think it makes sense to focus on making TS support more robust in the future, though I’m still of the mind that folks should just use typescript-eslint. — https://github.com/babel/babel/issues/10752 的 description

綜上所述,Typescript 的 linter 對於從 JavaScript 轉換過來的人來說,體驗還有待改善。

和 React 的相容性不太好

TypeScript 和 React 搭在一起,會有一些語法需要調整,以及意想不到的 issue。

  • generic 的語法會被 jsx 影響

上面的寫法看似正確卻會噴錯,因為 tsc 的 parser 無法分辨 <T> 到底是 jsx,或是 generic。所以你必須寫成下面那個樣子,加個逗號變成 <T,>

  • React.FunctionComponent 不能回傳 children。

如果要對 React component 做型別標記,一定會用到 @types/react 這個套件。裡面包含了社群對 React 的型別標記。其中React.FunctionComponent 常被用來標記 function component 的型別,。

今天我有一個直接回傳 children 的 function component,我很自然的用 React.FunctionComponent 標記型別:

然而這會噴出令人困惑的型別錯誤:

不只 children,回傳字串或陣列也有同樣的 error。

經過一番 google 之後,會發現這個 issue 在 2017 年就存在了。背後不只是 DefinitelyTyped 的問題,而是 TypeScript 把 jsx 的 return type 和 function component 的 return type 混在一起了

issue 的 PR 在三年後的今天仍然還沒 merge。如果要暫時讓它 work 的話,只好在上面包個 Fragment:

但這不美觀、效能也會變差、在 React Developer tool 上也會多出無用的雜訊。

或者,你可以不用 React.FunctionComponent,直接當作一般的函數標記 props,但這樣就會損失對 static property(如 displayName propTypes)的型別檢查。

大致上這些問題都有解,但就是不太令人滿意、花了不少時間尋找解法。而且只花了一點時間就發現這些問題,令人擔心或許還有一些未知的 issue。

學習曲線

對 JavaScript 開發者來說,寫 TypeScript 是完全不一樣的體驗。即使我們在動手轉移到 TypeScript 之前,已經在讀書會上讀了大半的 Programming TypeScript,對 TypeScript 有些基礎的了解,仍然會遇上很多預期之外的錯誤,要花上一段時間學習。

至於 React,幾乎沒有如何為 component 標記型別的官方文件。無論 ReactTypeScript ,都只有少量的提及如何用 TypeScript 撰寫 React 程式碼。真正有用的幾乎都是由開發者們撰寫的 cheatsheet(例如這個這個這個)。所以困難的型別標注如 HOC(High-Order Component),只好在一個個 cheatsheet 中穿梭和嘗試,直到 tsc 不再噴錯。

而以上的困難,如果專案只有一個人開發還好。更困難的是,讓團隊中的每一個人都能熟悉 TypeScript,了解踩過的雷,並且知道型別標記怎麼寫。無形中增加許多溝通和訓練的時間。

結論

以上幾點,讓我們覺得此時導入 TypeScript 或許還是太早了。就我們的現況,導入 TypeScript 可能會有一定程度地損害到產品開發的效率,所以最後決定先暫時擱置。

我們不是完全放棄 TypeScript — 在團隊規模越大、專案越多的時候,TypeScript 的型別檢查就越有幫助。未來如果前端團隊擴大到了 10 人、20人,有了更多的專案和模組,而且 TypeScript 改善了 React 的開發體驗,也會再嘗試看看的。此外,就算不轉換到 TypeScript,也可以透過手工撰寫型別宣告檔,讓 VSCode 得到型別資訊、取得一些開發體驗的好處。例如知名的 UI 函式庫 material-ui 便是這麼做的。

最後,能夠讓我們做出「TypeScript 此時還沒有那麼值得」的判斷,一部分或許是因為,在 iCHEF 已經有許多對軟體工程的實踐能夠避免我們不小心犯錯。型別安全雖然有價值,卻也不是一定立刻就要:

  • CI(build, lint & unit test),每個 PR 一定要通過才能進入主幹。
  • Code Review,每個 PR 一定要通過才能進入主幹。
  • QE 的嚴加把關
  • Design Review
  • 友善、blameless 的討論環境

每間公司、每個團隊或許情況不同,但在 iCHEF,這些就像陽光空氣水一樣自然。

目前 iCHEF 也在大量徵才中 — 前端、後端、iOS、QE、PM、設計都有開缺,歡迎參考。

Bonus: 如果你也想轉移到 TypeScript 的話

動手之前,不妨讀讀這些:

  • TypeScript at Slack:Slack 團隊轉移到 TypeScript 的分享,包含他們的痛點、轉移過去的收穫和代價。
  • Adopting TypeScript at Scale:Airbnb 轉移到 TypeScript 的 talk,在轉移的策略有不少著墨。在演講中也提到,airbnb 過往的 bug 裡有 38% 可以被 TypeScript 捕捉,算是談到 TypeScript 時少有的統計數字。
  • TypeScript Tax:Programming JavaScript Application (之前誤筆為 You Don’t Know JS,感謝讀者 kkshyu 的指正) 的作者 Eric Elliott 所寫的文章,主張「TypeScript 很棒,但代價不斐,而且和 JavaScript 比起來並沒有贏很多」,詳細列出了轉移到 TypeScript 可能會有的好處和壞處。儘管有點主觀,但仍然值得在動手前一讀,也許會打消你的念頭。

如果真的要動手,向你推薦:

  • React TypeScript Cheatsheet:包含了許多前人在 React 專案上使用 TypeScript 的經驗談和最佳實踐。儘管有點雜亂但仍有價值。
  • Effective TypeScript:仿照 Effective XXX 系列的風格,列出許多使用 TypeScript 要注意的事情,很值得想深入學習 TypeScript 的人一讀。此外,第八章詳細的介紹轉移到 TypeScript 所需的步驟和解法,會讓你對轉移的流程有個了解。最近終於出中譯本了。
  • ts-migrate:這是 airbnb 為了大量轉移到 TypeScript 自行開發的工具,它可以把指定資料夾底下的 JavaScript 自動轉換成 TypeScript,並且在出錯的地方加上 @ts-expect-error comment,最終就得到一個可以通過型別檢查的 TypeScript codebase。比起手改檔案要方便許多。雖然事後還是需要手動修改型別、解決 error,但很適合短時間轉移到 TypeScript 的場合(對,我們的嘗試也有用到它)。更詳細的介紹可以看這篇 blog

iCHEF

iCHEF 的技術分享

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store