TypeScript 寫久了,越來越覺得定義各種類型,介面啊其實都是在寫編輯器自動提示的配置而已。而且現在 ts 的各種高級類型越來越多,整個類型系統甚至可以看作是一套函數式工具庫。用它不難,用好它其實挺難的,其中的差別我覺得就像前端從面向 dom 的編程和轉變為面向數據驅動編程那樣,你得首先有類型思維,因為它一定程度上還會反過來影響你的 api 設計和數據結構。

先談下關於用不用 TypeScript 的個人觀點:

首先從個人的學習成長來說,一定要用!畢竟是一條目前很多公司都挺看重的技能點,而且學習使用的過程中多多少少還會提升你編碼的嚴謹性和 api 的設計能力。

然後從團隊來說,一句話:量力而為!實話實說,對於開發效率的提升影響不大,甚至是負面的:因為對於團隊當中可能沒有類型思維,沒有強迫症,也不願意去為了實現某個完備的類型提示而花功夫去搜索學習的同學,讓他們寫 ts 代碼,說實話就是在降低開發效率和噁心其他同學,他還會反感設計這一套方案的人。。

另外覺得網上很多所謂 TypeScript 解決的若干痛點其實多少有些誇大其詞了,比如:提前發現一些可能由於 undefined,類型不匹配導致的數據引用錯誤這一點其實仔細想想首先出現的概率沒那麼大,就算出現瞭解決該問題的時間也會小於你定義類型的時間(ts 之前大家不都是這麼過來的麼。。)然後上述優點的背後其實是你在使用某個數據之前,設計某個函數之前,全量地思考過類型隱患,然後去做了各種定義。換句話說,你原本就對類型敏感,能寫出一份完備類型定義,就算不用 ts ,也沒啥問題;而那些 any,類型推導直接乾的就算用了 ts,也有這問題啊!

用 TypeScript 只是因為爽!

實際開發當中真正因類型的引入收益最多的部分還是得回歸到 ts 的類型提示上面來,也就是標題上說的面向編輯器編程,各種代碼提示我覺得纔是廣大程序員的真正爽點,別人問我為什麼用 ts,我就只會說:哪怕我要花點時間甚至花大時間去定義一個類型,當我在編輯器上輸入了一個括弧,一個點,編輯器就知道我要幹啥的時候是真爽啊!提升效率?不存在的!

還有對於喜歡造輪子的同學,ts 有一個天然的好處就是你寫的文檔會省好多事,甚至類型約束本身比文檔來的更好用,編輯器就會直接告訴調用者該傳哪些參數,返回什麼數據。

閑聊篇:

前兩天維護過這麼一段有問題的代碼:

authors.map(author => {
if (author.name === user) {
return {
name,
status: xx
}
}
})

很明顯,返回的 name 欄位有問題,但瀏覽器不會報錯,因為 name 存在於 window 對象中。憑直覺應當修改為:

{
name: author.name,
status: xx
}

可是又由於上面有這麼一段:

author.name === user

理論上變數被命名成 user,它應該是個對象類型,而返回的 name 應當是個字元串類型。這種時候為了保險就只能去 review 上下文或者調試輸出了。這種時候是確確定定覺得 ts 大法好啊,如果上面那個函數體是 ts,並定義了返回值類型,首先就不會有這個錯誤,就算有我改起來也很有信心了。

any 是不是任何時候都不推薦使用?

好多人已經到了談 any 色變的程度了,這其實是又走向了另一個極端。any 被設計出來肯定是有使用場景的,合理使用是可以減少一些開發負擔。比如下面這個神奇的類:

class Test {
private x: number | string = hello

private test1() {
(this.x as any).toFixed()
}

test() {
this.x = 3.1415926
this.test1()
}
}

顯然調用 test1 的唯一入口就是外部調用該類的 公共方法 test,而該入口一定會把屬性值 x 設置成 number 類型,所以你在 test1 這個私有方法中把 x as 成 number 或者 any 沒啥區別。而且這裡只是為了舉個例子,實際 x 的類型可能會引用自某個三方包裏的某些定義起來非常繁雜的類型,as 起來會很不方便,這時候是推薦你用 any 直接乾的。

還有類似這種情況:

class SomeComponent extends Component{
private data: YourDataType

async initData () {
// 假設 fetch 在不傳泛型的情況下返回 any 類型
const { id } = await fetch(/xxx)
this.data = await fetch(`/data?id=${id}`)
}
}

假設第一次 fetch 的數據在當前場景下只用到了少數幾個欄位,且在本次請求後就再也沒有使用過。那麼對於 initData 這個函數來說,第一次 fetch 回來的數據類型你完全可以使用默認的 any 類型,而不必專門去定義一個通篇只有這一處會使用到的類型;而 this.data 這種顯然在組件生命週期中會多次引用的數據,類型的聲明就很有必要了。

ps:當然了,理論上你應當在業務中提前定義好獲取各類數據的方法,或者各類常用數據的類型,那麼上述代碼很可能就是下面這樣的,也就不用糾結是否使用 any 了。

import { User } from types
async initData () {
// 返回值自帶類型
// const { id } = await getCurrentUser()
// 其他模塊中定義過該類型
const { id } = await fetch<User>(/user)
}

而換句話說,如果:

  • 某些數據不是完全由你託管的私有數據
  • 你不只是想調用某個方法,修改某個數據,而是需要完整的類型提示來引導之後的編碼
  • 該數據你不止一處會引用到

那最好就不要 any 了,老老實實去聲明成目標類型,這種纔是常態。

善用類型推導:

function test() {
return {a: 1, b: 2}
}
const data = test()
data. // 這個時候編輯器就已經會提示你 data 內部會有哪些數據,分別是什麼類型

而什麼時候才需要明確地聲明上面 test 函數的返回值?就是未來這個函數很大可能會被別人維護,如同我開篇提到的第一個例子,這種情況下返回值類型的定義就有必要了。

幾個關於類型思維的問題:

一、設計一個函數,它的參數是:

  1. 一個任意函數
  2. 該函數原本需要接收的參數

返回值為該函數原本會返回的值

先拍腦袋寫一個:

function test(fun: Function, ...args: any[]) {
return fun.apply(null, args)
}

好吧,上述就是典型的用 ts 寫出來的 js 代碼,只是實現了上述需求的功能,基本沒有實現上述每一條需求所隱含的類型信息,我們來逐條看:

一個任意函數:約束了第一個參數的的接受值類型為 Function

該函數原本需要接收的參數:約束了其他參數的數量和類型都應當與傳入函數參數保持一致返回值為該函數原本會返回的值:約束了返回值類型為傳入函數調用後的返回值類型

下面是相對正確的寫法:

type AnyFunction = (...args: any[]) => any
function test<T extends AnyFunction, P extends Parameters<T>>(fun: T, ...args: P): ReturnType<T> {
return fun.apply(null, args)
}

相關閱讀:strictBindCallApply

通過上面的示例,所謂的類型思維的一個體現就是:你能不能察覺到一個功能需求中隱含的類型訴求。而面向編輯器或是面向類型編程就是你這個函數實現了之後在編輯器裏再真實地調用一下,看看代碼提示,錯誤警告等是否符合你的預期,如果不符合你願不願意花時間去完善或者去查資料。過程中我也經常會遇到一些疑難雜症,比如:

如何引用一個具有泛型的子類型?

如何從一個函數數組中獲取各個函數返回值的合併類型?

二、基於 express 設計一個 action 函數

這個問題的背景是:希望通過 action 函數來統一返回 express-router 的 callback。作為後端項目的 RequestHandler 來說,常見的需求就是統一 Response 格式,鑒權處理等。這裡想專門說一下它的鑒權部分,因為對於後端服務來說,不是所有的 api 都需要鑒權,因此該函數能夠通過某種方式來設置是否需要鑒權,剛開始想的方案是:

const getUser = action((req, user) => user) // 這是一個需要鑒權的行為
const getBooks = action((req) => getSomeSthByReq(req)) // 這是一個不需要鑒權的行為

很容易看出,這是一個很經典的通過 callback 函數參數數量來控制 action 函數的行為設計。而且這裡的user參數既可以控制是否鑒權,又恰好可以把鑒權結果,也就是用戶信息提供給 callback 函數,一石二鳥,簡直完美!雖然目前為止和 typescript 的關係還不是很大。

然而,隨著 api 的逐漸增加,發現該設計會有以下問題,比如:

const getUser = action((req, user) => user) // req 參數沒有被用到

// 希望登陸的用戶纔可以查看 productions
// 可是 productions 的獲取並不依賴 user 參數,所以結果就是 req 和 user 兩個參數都沒有用到
// 參數的浪費造成的可能不僅僅是內存浪費問題,首先要面臨的就是各種 linter 的報錯...
const getProductions = action((req, user) => getProductionsFromDb())

基於以上,首先咱們來解決參數的利用問題,不難想到把 requser 參數放到一個對象中,然後把該對象作為一個參數傳出去:

const getUser = action(({ user }) => user) // 通過使用 es6 的結構,簡潔而優雅

解決了參數的利用問題,再來解決鑒權的控制問題。顯然如果你不使用什麼黑科技的話你是無法通過 callback對象參數的欄位來控制action的行為了,而且即使可以,也同樣會有上述 getProductionsuser參數的浪費問題。所以再給action提供一個參數吧,就叫它authorize

// 通過設置 authorize 為 true 來控制鑒權
const getProductions = action(() => getProductionsFromDb(), true)

好了,到現在依然沒有和 typescript 扯上什麼關係,不過接下來。。。

const getXX = action(({ user }) => getXXFromXX(user.id))

忙中難免出錯,上述代碼用到了 user,卻沒有設置 authorizetrue,這麼幹肯定會導致錯誤,而且這個錯誤只有在運行時才會被發現(又有人要喊你回去改 bug 了)。

那麼,上 typescript 大法吧!上述場景的 ts 解決方案非 函數重載 莫屬了(我就說 ts 的文檔有問題吧,比如函數重載這四個字我就沒法直接給個官方文檔的鏈接)

function action(callback: (data: { req: Request }) => ResponseData): Action
function action(callback: (data: { req: Request; user: User }) => ResponseData, authorize: boolean): Action
function action(callback: any, authorize?: boolean) {
// 實現略
}

有了對 action函數的類型約束,如果你再用到了user參數,而沒有設置authorize,那麼請接受 ts 類型檢查的問候吧!

三、針對 React 的 高階組件的若干問題

平時使用 React 的同學對 HOC 肯定不陌生,如果你使用的是 tsx,是否考慮過如下問題:

  • HOC 後的組件的 props 類型你是否認真考慮過注入,修改,刪除這三個場景的類型實現?
  • HOC 函數本身是否對傳入組件的類型做過約束?比如該函數只接受 Input 類組件。
  • 如果原始組件具有泛型,HOC 後如何保持泛型傳入?
  • 如果原始組件具有靜態方法/屬性,HOC 後如何保持引用?

這些問題由於時間關係,我準備就挑一個來說:網上好多關於 ts 的 HOC 實現一般都要求傳入原組件的 Props 作為泛型參數,可實際開發中不是所有的組件都會老老實實導出一個 Props 類型給你用,這種時候你可以藉助 React.ComponentProps 來拿到。

最後,總感覺 TypeScript 的 官方文檔 寫的有問題啊,查閱起來不是很方便,有內置的很多工具類型或語法,比如:Exclude、Pick、keyof 等分散在各個頁面中,甚至就沒有提到。我想知道有沒有一個統一的地方可以查閱以上所有。

推薦閱讀:

相關文章