本文原創:nanyifei
前言
近幾年,前端領域得到了日新月異的發展,各種新技術、框架層出不窮,前端的圈子越來越大。在 Github
的官方統計中,JavaScript
已經連續多年在語言榜上拔得頭籌。隨著應用越做越大,業務越來越復雜,動態語言自然有其靈活性好的優點,但是同時會因為無法保證合理的類型而引起不必要的并且隱藏的 bug
。并且在業務代碼流轉的時候,如果代碼沒有清晰的注釋,很難閱讀。
jsDoc
+ eslint
配合 vscode
使用能夠減少許多閱讀代碼的不安和不適情緒,但是規則約束過于嚴格常常會出現因為不及時寫注釋而帶來的大面積飄紅。注釋雖然可以解決一部分代碼難以理解的問題,畢竟由于是靜態的文本,注釋無法靈活的流動。
TypeScript
在繼承了 JavaScript
所有特性的基礎上,給原生 js 插上了靜態類型及增強的面向對象能力的翅膀,并且持續推進部分 es 提案語法成為標準。在打敗了 Flow
、CoffeeScript
等一眾對手后,成為社區的主流。在 Github
的年度語言榜上一路高歌,2020 年已經排到了第 4 位,并且還有持續進步之勢。
社區內優秀的作品數不勝數,為什么要寫這篇文章呢?我們受到了一種啟發:在科學研究領域,有一類 paper
專門對一個研究點好的研究現狀進行匯總,方便其他相關工作者能夠了解該領域的最佳實踐,即綜述。我們想做的事情類似,把每一個前端領域的點相關的好文章整理起來,逐漸匯聚成面,以期望形成完備的知識體系,把最優秀的資源分享給大家。
這是一個開始,也是在2021年第一個月。第一篇讓我們與 TypeScript 愉快玩耍。
基礎篇
類型系統
從語法層面上講,TypeScript 可以做如下理解:
TypeScript = JavaScript + 類型系統 + 增強的面向對象語法 + ES語法提案 + 編譯器
總的來說,如果開發者有扎實的 JavaScript
基礎,那么 Ts
上手會很快。
首先是類型系統,Ts
引入了 Boolean
、Number
、String
、Symbol
、Array
、Enum
、Any
、Unknown
、Tuple
、Void
、Never
等類型。類型系統繁多復雜,但是每種類型都有其存在的價值。
這里先推薦一些文檔及文章,從中就可以窺探類型系統的秘密。
學習的一個好方法就是結合官方提供的在線編譯器 Playground 與一篇詳盡的文章。官方的 HandBook
自然是最全面直觀的一手資料,當然 Github
及社區內也有很多優秀的文章及開源項目介紹 Ts,這里不勝列舉。
在上述這些類型中,有許多需要我們額外關注的細節點:
枚舉類型
我們都知道枚舉類型有數字、字符串、異構及常量枚舉。
- 尤其需要注意的是,常量枚舉是由
const
關鍵字修飾的枚舉,在編譯階段被刪除,也就是說 常量枚舉無法編譯出任何 js 代碼。小伙伴們可以在 Playground 上親自試一試。
const enum Directions {
Up,
Down,
Left,
Right
}
// 對應編譯后的 js 代碼只有嚴格模式的聲明語句
"use strict";
- 除了字符串枚舉,編譯后都會生成一個雙向的映射關系。這就意味著如果我們使用
Object.entries
去獲取枚舉對象的鍵值對時,會有如下的結果:
enum Directions {
Up = 0,
Down = 1,
Left = 2,
Right = 3
}
const DirectionsList = Object.entries(Directions)
// [["0", "Up"], ["1", "Down"], ["2", "Left"], ["3", "Right"], ["Up", 0], ["Down", 1], ["Left", 2], ["Right", 3]]
可以看到雙向映射關系會產生對稱的鍵值關系。如果改成字符串枚舉,就會有如下的結果:
enum Directions {
Up = '0',
Down = '1',
Left = '2',
Right = '3'
}
const DirectionsList = Object.entries(Directions)
// [["Up", "0"], ["Down", "1"], ["Left", "2"], ["Right", "3"]]
少了雙向映射,就可以用枚舉存儲一些常見的 select
組件要求的內容了。
頂級類型和兜底類型
ts 中有兩個頂級類型 any
和 unknown
,還有一個兜底類型 never
。當然 any
類型是類型系統的逃逸倉,也可以為所有類型兜底。大量使用 any 類型會帶來不可控制的結果,ts 帶來的靜態類型檢查機制無法生效,因此 TypeScript
3.0 引入了 unknown
類型。
顧名思義,unknown
是“未知”的含義,即任何明確的類型都可以賦值給 unknown
類型,而不能把 unknown
類型的變量賦值給任何一個有明確類型的變量。unknown
類型可以理解為:如果一個變量類型暫時無法確定,但是它在未來的某一時刻一定會有明確的類型,我們就可以在初始化該變量的時候,給一個 unknown 類型。因此其使用也需要明確一個原則:使用前,要對其做類型判斷,否則會語法報錯。我們也可以在各大框架的源代碼中看到 unknown
的身影。
// Vue3 源碼中關于 unknown 的使用
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
這里更多的細節可以參考知乎問答:寫TypeScript時,什么時候用any?什么時候用unkown?有沒有規律或準則?
另一方面,其實需要投入更多關注點的是兜底類型 never
,直觀意義上講用于:
- 拋出異常的函數
- 無限循環
實際上,never
類型是所有類型的子類型,廣泛應用于工具類型中,主要用途為 discriminated union,即類型收窄,可以幫助我們寫出類型更安全的代碼。這里更多細節可以參考:
Object object {}
這是三種容易混淆的類型:
-
object
是比較常用的類型,區別于基本類型,用于表示非原始類型。 -
{}
是一種比較特殊的類型,不需要顯式地給類型定義,當初始化一個變量為{}
,ts 會自動推斷該變量的類型為{}
。類似于Object.freeze
的能力,不能直接對變量進行屬性值的操作。
有關三者的詳細區別請查看 一文讀懂 TS 中 Object, object, {} 類型之間的區別
斷言
斷言的作用是讓開發者告知 ts 一些相關變量的類型信息,可以分為類型斷言、非空斷言、確定賦值斷言及 const
斷言。
const 斷言
在 Ts3.4 中引入了一種新的斷言 const assertions
,使用了該斷言后,字面量不能被擴展。變量的類型被約束在當前字面量的形狀上,并且屬性只讀。可以使用 as const
及 <const>
兩種方式來聲明。
// type 的類型為 {text: string}
const type = { text: "hello" } as const;
在 Vue3
中,組件的 props
可以結合 PropType
進行 ts 類型聲明。如果我們需要抽離一部分公共的 props
存儲到 ts
文件中,在使用的時候在組件內引入,那么這個 props
對象就需要使用 const
斷言來保證其只讀的特性。可以在 vue-next
源碼中 runtime-core/src/apiDefineComponent.ts
里關于 defineComponent
源碼中也可以發現這樣的特征。
// overload 4: object format with object props declaration
// see `ExtractPropTypes` in ./componentProps.ts
export function defineComponent<
// props 是只讀的
PropsOptions extends Readonly<ComponentPropsOptions>,
RawBindings,
D,
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>,
EE extends string = string
>(
...
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
同時 const
斷言還在 redux
的使用中有應用,具體詳情請閱讀:
面向類型編程
Ts
不僅僅從語法層面提供了靜態類型檢查機制,更是給了開發者更加廣闊的空間將代碼寫的更加靈活,提升寫代碼的幸福感與安全感。引入了 ts
之后,就是以一種思維方法去開發需求,詳情查看 TypeScript - 一種思維方式。如何讓應用中的類型流動起來,能夠復用甚至衍生,同時盡可能保證應用類型安全對于開發者而言是一個不斷成長的過程。在這其中,泛型起到了至關重要的作用。
泛型
泛型是一種描述程序中存在的類型及類型之間關系的方式,泛型單獨存在是沒有意義的,正如其名稱一樣,太廣泛了,因此需要配合泛型約束,通過合理的約束能夠寫出健壯性很強的代碼。
只要是用 ts 寫的第三方庫中,都可以隨處看到泛型的身影。而在類型編程過程中,對類型取其索引簽名、做類型映射、類型修飾等等操作也如家常便飯一般常見,因此 ts 及社區提供了很多好用的工具類型,將類型編程演繹到極致。
工具類型
在工具類型中我們可以經常看到 never
類型的身影,用于幫助在條件類型中將類型收窄。
在工具類型中,我們還會經常見到 infer
關鍵字的身影,后面跟上一個待推斷的類型。這里有一個延遲推斷的思想,與 js
中回調函數的思想類似,只有 ts 獲取到足夠的類型信息后才能推斷出類型。常見的 infer
使用在 ReturnType
這個工具類型中,用于獲取函數類型的返回值類型。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
這個工具類型可以做如下解釋:如果 T 這個類型滿足了 (...args: any[]) => infer R
這個函數類型的所有形狀要求,而且函數的返回值類型能夠被推斷出來,那么函數的返回值類型為 R
,否則類型推斷結果為最寬泛的類型 any
。
有了這樣子的思想,infer
還可以做更多有意思的事情,比如:在一個 Tuple
類型中,刪除掉第一個位置的元素。就可以如下做:
type Tail<T extends any[]> = ((...args: T) => void) extends (a: any, ...args: infer P) => void ? P : never;
type A = [string, number, boolean];
type AT = Tail<A>
const at: AT = [1, false];
這個想法來源于我們在函數的 arguments
對象上進行提前解構,拿到第一個入參及后面的所有入參。如果類型推斷可以推斷出解構后的參數形狀,那么就可以得到我們想要的結果。同時,這里一定要注意一個細節點,如果 Tail
這個工具類型定義為如下:
// 對比上面,我們僅僅去掉了 extends 之前這個函數形狀的括號
type Tail<T extends any[]> = (...args: T) => void extends (a: any, ...args: infer P) => void ? P : never;
如上定義會一直返回一個 never
類型,原因在于如果不加括號,ts 的條件類型推斷會變成 void extends (a: any, ...args: infer P) => void
,這顯然是不可能滿足的,因此會一直返回 never
類型。
應用篇
這一部分中,我們列舉了部分社區內好的技術實踐,既有在熱門框架的基礎應用,也有優秀的工程師的技術沉淀,部分鏈接可能需要梯子訪問。
React
在 Class Component 中的應用
- 強烈推薦:Ultimate React Component Patterns with Typescript 2.8
- TypeScript 2.8下的終極React組件模式(上面文章的翻譯版)
- Ts 寫 React 高階組件:React Higher-Order Components in TypeScript
- 知乎問答:TypeScript 如何完美地書寫 React 中的 HOC?
在 Hooks 中的應用
- React + TypeScript + Hook 帶你手把手打造類型安全的應用
- typescript-cheatsheets/react
- React Hooks in TypeScript
- 阿里的 hooks 庫
- 著名的 react-use
Vue
Vue2 中使用裝飾器與 Class Components
方式。
Vue3 的 Composition Api
提供了更加優雅的邏輯復用能力,因此相關的邏輯工具包是學習 Vue3
與 ts
的好資料,并且可以為簡化邏輯代碼,為業務賦能。
Node
Ts 在 node 端的應用主要圍繞裝飾器展開,即便 js
中的裝飾器與 ts
中的裝飾器存在諸多差異。但是有了裝飾器,就可以將元編程、依賴注入的思想應用進去。
裝飾器相關
框架
- Midway
- Nest
- Overnight
- Loopback
結語
TypeScript 是未來前端的大勢所趨,社區活躍,用戶量大,因此能夠產出大量的高質量庫及技術文章。本文嘗試以一種綜述形式,將在學習過程中閱讀過的印象深刻,有深度啟發的好文章分享出來,希望能夠對大家的學習有一定的幫助,同時也感謝以上附錄文章作者的杰出貢獻。