本文首發於個人我的博客。react
不知道是就我這樣,仍是你們也是,最近的內容圈子裏關於 TypeScript 的文章滿天飛,各類 TypeScript 有多好、多受歡迎,要不就是 TypeScript 的教程、實踐。恰恰我在這時候有了寫這篇文章的想法,搞得頗有跟風蹭熱度的嫌疑。git
做爲一名堅持原創的做者,我並不想把市面上隨手可得的東西,換一種方式再講給你們聽,這樣不只是在浪費你們的時間,也是在浪費我本身的時間。我所理解的爲社區作貢獻,應該是可以填補當前環境下的一些空白,去作一些真正有意義的事,而不是擺出一副資深的樣子,去轉發或是創造一些重複的內容。github
今天這篇文章,雖然有跟風的嫌疑,但我向你保證,內容依然是絕對的原創。若有巧合,那麼英雄所見略同。typescript
上手 TypeScript 並不難,有 JavaScript 基礎的同窗,花個一天時間過一遍文檔,基本就都清楚了。若是你恰好還有 Java、C# 等後端語言的基礎,那麼其中關於 OOP 的一些概念相信你必定會以爲很是眼熟。後端
若是你剛看完文檔就開始準備把 TypeScript 用到項目中去,那麼恭喜你,你很快就會遇到各類坑,並且你沒法直接從文檔中尋找到對應的解決方案。這篇文章的存在,就是但願可以填補這中間的空白,幫助各位順利的把 TypeScript 落地到項目中。數據結構
這即是標題中「第二天」的由來。若是你尚未看過 TypeScript 的文檔,那麼這篇文章如今還不適合你,建議先收藏起來,等看完了文檔再回來。編輯器
若是你已經準備好了,那咱們開始吧。函數
每一個人接觸 TypeScript 的緣由不一樣,有的是被人安利,有的是由於團隊在用,有的是由於用了 Angular。但無論由於什麼入了這個坑,咱們都須要明白:TypeScript 並不是必須。工具
TypeScript 適合大型項目,小型項目最好仍是繼續用 JavaScript。這已是業內的一個共識。ui
TypeScript 能夠簡單理解爲 JavaScript + Types。從工程效率的角度上講,Types 的部分屬於額外的工做量,若是不能給項目帶來足夠的收益,去平衡掉其引入的成本,那麼這項投入就不是很值得。
若是隻是官網之類的小型項目,類型不類型的並不重要,不必爲了用 TypeScript 而用 TypeScript。但隨着項目的規模和複雜度的增長,代碼質量、溝通成本等問題開始浮現,而這偏偏是類型系統可以解決的問題。經過類型檢測,咱們能夠更早的發現潛在的類型錯誤,進行主動防護,進而提升代碼質量;經過類型定義,咱們能夠更加直觀的描述咱們的數據結構,下降團隊做業中的溝通成本。
所以,要不要用 TypeScript,取決於你項目的類型以及規模,不要盲目跟風。
不少人對 TypeScript 有一個誤解,以爲有了靜態類型的代碼已經足夠「自解釋」,就不須要 jsDoc 一類的註釋了。
靜態類型描述的是數據的結構,而註釋描述的是數據的做用,二者解決的是不一樣的問題,彼此之間並不衝突。
好比下面這段 JavaScript 代碼:
function convert (val, config) {
// some code
}
複製代碼
不難看出這是一個轉換函數,接收一個待轉換的值,以及一個配置對象,但咱們並不知道這個函數用來轉換什麼,配置對象又有哪些參數。
如今咱們用 TypeScript 來重寫一下,補充一些類型定義。
function convert (val: string, config: { x: string, y? :boolean}): string {
// some code
}
複製代碼
如今咱們知道了這是一個用於處理字符串的轉換函數,配置對象有兩個參數,一個是必選的字符串,一個是可選的布爾值,最後返回的也是一個字符串。但具體到業務中,這個函數用來轉換什麼樣的字符串,咱們仍是不太清楚。
/** * @description 對手機號進行編碼,隱藏其中一部分,如:13812345678 -> 138****5678 * @param val 待編碼的手機號 * @param config 配置選項 */
function convert (val: string, config: { x: string, y? :boolean}): string {
// some code
}
複製代碼
加上註釋以後,一切就都清楚了:這是一個對手機號進行編碼,將其中一部分替換成其餘字符,以保護用戶隱私的函數。
因此你看,TypeScript 並不能徹底替代 jsDoc 的做用,該寫的註釋仍是得寫。
固然對於上面的例子,若是是一個單純的工具函數,咱們徹底可使用更加直觀的命名,好比 encodeMobile (mobile, config)
,但若是這是某個類中的成員函數,那麼可能就不可避免地會出現示例中的寫法。總之,你明白個人意思就好,就不要鑽牛角尖了。
對了,得益於 TypeScript 的類型系統,@param
不須要再指定數據類型了,只要對變量的用途進行描述就行了。若是你配置了 Lint 工具,它也會提醒你優先使用 TypeScript 來定義類型,不要重複定義。
以前咱們在用 JavaScript 寫 React 時,對文件的擴展名沒有什麼特別的要求,*.js
或者 *.jsx
都行。
但在 TypeScript 中,若是你要使用 JSX 語法,就不能使用 *.ts
,必須使用 *.tsx
。若是你不知道,或者忘了這麼作,那麼你會在使用了 JSX 代碼的地方收到類型報錯,但代碼自己怎麼看都沒有問題。這也是剛上手 TypeScript + React 時幾乎每一個人都會遇到的坑。
關於這一點,TypeScript 只是在官方教程的示例代碼中直接用了 *.tsx
,但並無明確說明這一問題。
React 則在它的官方文檔中說明了這一規則:
In React, you most likely write your components in a
.js
file. In TypeScript we have 2 file extensions:
.ts
is the default file extension while.tsx
is a special extension used for files which contain JSX.
在使用 React 時,咱們一般會把組件寫在一個
.*js
文件裏。在 TypeScript 中咱們有兩種文件擴展名:
.ts
是默認的文件擴展名,而.tsx
是用於包含了 JSX 的文件的特殊擴展名。
其實上面這段話也沒有明說在 *.ts
中使用 JSX 會報錯,因此即使有人看到了這段話,可能也覺得只是像 *.jsx
同樣多了一種選擇,並無太當回事……直到遇到問題。
上手 TypeScript 以後很快咱們就發現,即使是原生的 DOM、或是 React 的 API,也常常會要咱們手動指定類型。但這些結構並非簡單的 JavaScript 原始類型,在使用 JavaScript 編寫相關代碼時候因爲沒有這種須要,咱們也沒關心過這些東西的類型,忽然問起來,還真不知道這些類型叫什麼名字。
不光是這些標準類型,一樣的問題在不少第三方的庫中也會遇到,好比一些組件庫會檢查你傳入的 Props。
在我看來,這中間其實缺乏了一部分的文檔,來指導新用戶如何找到所須要的類型。既然社區沒有提供,那就我來吧。
固然,讓每一個開發者都熟記全部的類型確定是不現實的,總不能每接觸一個新的庫,就要去記一堆類型吧。放心,世界仍是美好的,這種事情,固然是有方法的。
最直白的方法就是去看庫的 Types Definition,也就是那些 .*d.ts
文件。若是你恰好有在用 VS Code 的話,有一個很是方便的操做:把鼠標移動到你想知道它類型的代碼上(好比某個變量、某個函數調用,或是某個 JSX 標籤、某個組件的 props),右鍵選擇「Go to Definition」(或者光標選中後按 F12),就能夠跳轉到它的類型定義文件了。
若是你更習慣使用 VS Code 以外的編輯器,我相信時至今日,它們應該也都早就對 TypeScript 提供了支持。具體操做我不太熟悉,你能夠本身探索下(我一直用 VS Code,其它的不太熟)。
通常來講,這個操做能夠直接把你帶到你想要的地方,但考慮到類型是能夠繼承的,有時候一次跳轉可能不太夠,遇到這種狀況,那就須要你隨機應變一下,沿着繼承關係多跳幾回,直到找到你想要的內容。
對於不熟悉的類型,能夠經過這個方法去尋找,慢慢熟悉之後,你會發現,一些常見的類型仍是很好找的,稍微聯想一下英文的表達方式,配合自動補全的提示,通常都不難找到。
爲了方便初學者,咱們仍是稍微列舉一些常見的類型,找找感受:
TypeScript 自帶了一些基本的類型定義,包括 ECMAScript 和 DOM 的類型定義,全部你須要的類型均可以從這裏找到。若是你想作一些「純 TypeScript 開發」的話,有這些就夠了。
好比下面這張截圖,就是對 <div>
標籤的類型定義。咱們能夠看到,它繼承了更加通用的 HTMLElement
類型,而且擴展了一個即將被廢棄的 align
屬性,以及兩組 addEventListener
和 removeEventListener
,注意這裏使用了重載。
這裏的命名也不是隨便起的,都是在 MDN 上能夠查到的。
仍是以 <div>
爲例,咱們已經知道它繼承自 HTMLElement
,其實再往上,HTMLElement
繼承自 Element
,Element
又繼承自 Node
,順着這條路,你能夠挖掘出全部 HTML 標籤的類型。
對於一些 DOM 相關的屬性,好比 onclick
、onchange
等,你均可以如法炮製,找到它們的定義。
關於 TypeScript 的問題,有很多實際上是在使用第三方庫的時候遇到的,React 就是其中比較典型的一個。
其實方法都同樣,只不過相關的類型定義不在 TypeScript 中,而是在 @types/react
中。
React 的類型定義的名稱其實也很直觀,好比咱們常見的 React.Component
,在定義 Class 組件時,咱們須要對 Props 和 State 預先進行類型定義,爲何呢?答案就在它的類型定義中。
再好比,當咱們在寫一些組件時,咱們可能會須要向下傳遞 this.props.children
,但 children
並無被設爲默認值,須要咱們本身定義到 props 上,那麼它的類型應該是什麼呢?
到類型定義中搜一下關鍵字 children,很快咱們就找到了下面的定義:
全部 React 中 JSX 所表明的內容,不管是 render()
的返回,仍是 children
,咱們均可以定義爲一個 ReactNode
。那這個 ReactNode
長什麼樣呢?咱們經過右鍵繼續尋找:
看到這裏,咱們不光找到了咱們想要的類型,還順帶明白了爲何 render()
能夠返回 boolean、null、undefined 表示不渲染任何內容。
那麼事件呢?當咱們給組件定義事件處理函數的時候,也常常會被要求指定類型。仍是老辦法,找不到咱就搜,好比 onClick
不清楚,那咱們就以它爲關鍵字去搜:
據此咱們找到一個叫 MouseEventHandler
的定義,這名字,夠直白吧。
好了,咱們找到想要的了。不過既然來了,不如繼續看一下,看看還能發現什麼。咱們右鍵 MouseEventHandler 急需往下看:
看到了嗎,全部的事件處理函數都有對應的定義,每一個都須要一個泛型參數,傳遞了事件的類型,名稱也挺直白的。
Ok,事件的類型也被咱們挖出來了,之後若是須要單獨定義一個事件相關的類型,就能夠直接用了。
以此類推,無論是什麼東西的類型,均可以去它們對應的 @types/xxx
裏,按關鍵字搜,只要你的英語別太差,很容易就能找到。
咱們知道 Interface 是能夠多繼承的,extends 後面能夠跟多個其它 Interface,咱們不能保證被繼承的多個 Interface 必定沒有重複的屬性,那麼當屬性重複,但類型定義不一樣時,最終的結果會怎麼樣呢?
在 TypeScript 中,Interface 會按照從右往左的順序去合併多個被繼承的 Interface,也就是說,同名屬性,左邊的會覆蓋右邊的。
interface A {
value?: string
}
interface B {
value: string
}
interface C {
value: number
}
interface D extends A, B {}
// value?: string
interface E extends B, C {}
// value: string
複製代碼
obj[prop]
沒法訪問怎麼辦有時候咱們會定義一些集合型的數據,例如對象、枚舉等,但在調用的時候,咱們未必會直接經過 obj.prop
的形式去調用,可能會是以 obj[prop]
這種動態索引的形式去訪問,但經過動態索引的方式就沒法肯定最終訪問的元素是否存在,所以在 TypeScript 中,默認是不容許這種操做的。
但這又是個很是合理,並且很是常見的場景,怎麼辦呢?TypeScript 容許爲類型添加索引,以實現這一點。
interface Foo {
x: string,
y: number
[index: string]: string | number
}
複製代碼
這個方法雖然有效,但每次都要手動爲類型加索引,重複多了也挺心累的。包括在一些「配置對象」中,咱們甚至沒法肯定有哪些類型,有沒有一種更加通用、更加一勞永逸的方法。
固然有。
其實在 TypeScript 的官方文檔中就有提到這個方案,官方管它叫 OptionBag,大概就是指 config、option 等用於提供配置信息的這麼一類參數。我不是很肯定這究竟是個常規的英文單詞,仍是 TypeScript 中特定的術語(我的感受是前者),反正就這麼個意思吧。
簡單說來,咱們能夠定義下面這樣一個類型:
interface OptionBag {
[index: string]: any
}
複製代碼
這是一個很是通用的結構,以字符串爲鍵,值能夠是任何類型,而且支持索引 —— 這不就是 Object 麼。
以後全部須要動態索引的結構,或是做爲配置對象的結構,均可以直接指定爲,或是繼承 OptionBag
。這個方案以犧牲必定的類型檢查爲代價,換取了操做上的便利。
理論上講,OptionBag
能夠適用於全部相似對象這樣的結構,但不建議各位真就這麼作。這個方案只能是用在一些對類型要求不那麼嚴格,或是沒法預知類型的場景中,可以肯定的類型仍是儘量地寫一下,不然就失去了使用 TypeScript 意義了。
TypeScript 確實是個好東西,但世上沒有絕對完美的東西,實踐過程當中總會有那麼些阻礙完咱們前進的坑。可是掉坑裏並不可怕,只要有辦法能爬出來,那就都不叫事兒。
原創不易,堅持原創更是,但願這篇文章多少能給你們帶來一些收穫吧。