關於TypeScript , 你知道多少?


關於TypeScript , 你知道多少?

電影《降臨》中有一個觀點,語言會影響人的思維方式,對於前端工程師來說,使用 typescript 開發無疑就是在嘗試換一種思維方式做事情。

其實直到最近,我才開始系統的學習 typescript ,前後大概花了一個月左右的時間。在這之前,我也在一些項目中模仿他人的寫法用過 TS,不過平心而論,在這一輪系統的學習之前,我並不理解 TS。一個多月前,我理解的 TS 是一種可以對類型進行約束的工具,但是現在才發現 TS 並不簡單是一個工具,使用它,會影響我寫代碼時的思考方式。

TS 怎麼影響了我的思考方式

對前端開發者來說,TS 能強化了「面向接口編程」這一理念。我們知道稍微複雜一點的程序都離不開不同模塊間的配合,不同模塊的功能理應是更為清晰的,TS 能幫我們梳理清不同的接口。

明確的模塊抽象過程

TS 對我的思考方式的影響之一在於,我現在會把考慮抽象和拓展看作寫一個模塊前的必備環節了。當然一個好的開發者用任何語言寫程序,考慮抽象和拓展都會是一個必備環節,不過如果你在日常生活中使用過清單,你就會明白 TS 通過接口將這種抽象明確為具體的內容的意義所在了,任何沒有被明確的內容,其實都有點像是可選的內容,往往就容易被忽略。

舉例來說,比如說我們用 TS 定義一個函數,TS 會要求我們對函數的參數及返回值有一個明確的定義,簡單的定義一些類型,卻能幫助我們定位函數的作用,比如說我們設置其返回值類型為 void,就明確的表明了我們想利用這個函數的副作用;

把抽象明確下來,對後續代碼的修改也非常有意義,我們不用再擔心忘記了之前是怎麼構想的呢,對多人協作的團隊來說,這一點也許更為重要。

當然使用 jsdoc 等工具也能把對函數的抽象明確下來,不過並沒有那麼強制,所以效果不一定會很好,不過 jsdoc 反而可以做為 TS 的一種補充。

更自信的寫代碼

TS 還能讓我更自信的寫前端代碼,這種自信來自 TS 可以幫我們避免很多可能由於自己的忽略造成的 bug。實際上,關於 TS 輔助避免 bug 方面存在專門的研究,一篇名為 To Type or Not to Type: Quantifying Detectable Bugs in JavaScript 的論文,表明使用 TS 進行靜態類型檢查能幫我們至少減少 15% 以上的 bug (這篇論文的研究過程也很有意思,感興趣可以點擊鏈接閱讀)。

可以舉一個例子來說明,TS 是怎麼給我帶來這種自信的。

下面這條語句,大家都很熟悉,是 DOM 提供依據 id 獲取元素的方法。

const a = document.getElementById("a")

對我自己來說,使用 TS 之前,我忽略了document.getElementById的返回值還可能是 null,這種不經意的忽略也許在未來就會造成一個意想不到的 bug。

使用 TS,在編輯器中就會明確的提醒我們 a 的值可能為 null。

關於TypeScript , 你知道多少?

我們並不一定要處理值 null 的情況,使用 const a = document.getElementById('id')!可以明確告訴 TS ,它不會是 null,不過至少,這時候我們清楚的知道自己想做什麼。

使用 TS 的過程就是一種學習的過程

使用 TS 後,感覺自己通過瀏覽器查文檔的時間明顯少了很多。無論是庫還是原生的 js 或者 nodejs,甚至是自己團隊其它成員定義的類型。結合 VSCode ,會有非常智能的提醒,也可以很方便看到相應的接口的確切定義。使用的過程就是在加深理解的過程,確實「面向接口編程」天然和靜態類型更為親密。

比如說,我們使用 Color 這個庫,VSCode 會有下面這類提醒:

關於TypeScript , 你知道多少?

不用去查文檔,我們就能看到其提供的 API。 如果我們去看這個庫的源文件會發現,能有提醒的原因在於存在下面這樣的定義:

// @types/color/index.d.TS
interface Color {
toString(): string;
toJSON(): Color;
string(places?: number): string;
percenTString(places?: number): string;
array(): number[];
object(): { alpha?: number } & { [key: string]: number };
unitArray(): number[];
unitObject(): { r: number, g: number, b: number, alpha?: number };
...
}

這種提醒無疑能增強開發的效率,雖然定義類型在早期會花費一定的時間,但是對於一個長期維護的比較大型的項目,使用 TS 非常值得。

一種學習 typescript 的路徑

也許是因為,我之前從未系統的學習過一門靜態語言,所以從開始學到感覺自己基本入門了 TS 花的精力還挺多的。 學習 TS 的過程中,主要參考了以下這些資料,我對這些資料有著一些簡單的分析。

  • TypeScript handbook — book
  • TypeScript Deep Dive — book
  • TypeScript-React-Starter — github
  • react-typescript-cheaTSheet — github
  • Advanced Static Types in TypeScript — egghead.io
  • Use TypeScript to develop React Applications — egghead.io
  • Practical Advanced TypeScript — egghead.io
  • Ultimate React Component Patterns with Typescript 2.8 — medium
  • The TypeScript Tax — medium

在閱讀上述資料的過程中,我使用 TS 重寫了一個基於 CRA 的簡單但是很完整的前端項目,現在覺得,使用 TS 來開發工作中的常見需求,應該都能應對了。如果你是剛剛開始學 TS,不妨參照下面的路徑學習。

搭建 TS 運行環境

不要誤解,並非從零搭建。學習實踐性很強的內容時,邊學邊練習可以幫我們更快的掌握。如果你使用 React,藉助 yarn 或者 create-react-app,可輕易的構造一個基於 TS 的項目。

在命令行中執行下述命令即可生產可直接使用的項目:

# 使用 yarn
$ yarn create react-app TS-react-playground --typescript
# 使用 npx
$ npx create-react-app TS-react-playground --typescript

隨後如果需要,可以在tsconfig.json中添加額外的配置。

就我個人而言,我喜歡同步配置 TS-lint 與 prettier,已免去之後練習過程中格式的煩惱。配置方法可以參考 Configure TypeScript, TSLint, and Prettier in VS Code for React Native Development 這篇文章,或者看我的配置記錄。

如果你不使用 React,TypeScript 官方文檔首頁就提供了 TS 配合其它框架的使用方法。

理解關鍵的概念

我一直覺得,學習一項新的技能,清楚其邊界很重要,相關的細節知識則可以在後續的使用過程中逐步的瞭解。我們都知道,TS 是 JS 的超集,所以學習 TS 的第一件事情就是要找到「超」的邊界在哪裡。

這個階段,推薦閱讀 TypeScript handbook — book,這本書其實也是官方推薦的入門手冊。這裡給的鏈接是中文翻譯版的鏈接,翻譯的質量非常好,雖然內容沒有英文官方文檔新,不過學習新的東西最好還是從自己最熟悉的內容入手,所以不妨先看中文文檔。閱讀過程中遇到的示例,都可以在上面搭建的 TS-playground 中練習一下,熟悉一下。

TS 做為 JS 的超集,其「超」其實主要在兩方面

  • TS 為 JS 引入了一套類型系統;
  • TS 支持一些非 ECMAScript 正式標準的語法,比如裝飾器;

關於第二點,TS 做的事情有些類似 babel,所以也有人說 TS 是 babel 最大的威脅。不過這些新語法,很可能你早就使用過,本文不再贅述。

比較難理解的其實是這套類型系統,這套類型系統有著自己的聲明空間(Declaration Spaces),具有自己的一些關鍵字和語法。

對我來說,學習 TS 最大的難點就在於這套類型系統中有著一些我之前很少了解的概念,在這裡可以大致的梳理一下。

一些 TS 中的新概念

編程實際上就是對數據進行操作和加工的過程。類型系統能輔助我們對數據進行更為準確的操作。TypeScript 的核心就在於其提供一套類型系統,讓我們對數據類型有所約束。約束有時候很簡單,有時候很抽象。

TS 支持的類型如下:

boolean,number,string,[],Tuple,enum,any,void,null,undefined,never,Object。

TS 中更復雜的數據結構其實都是針對上述類型的組合,關於類型的基礎知識,推薦先閱讀基礎類型一節,這裡只討論最初對我造成困擾的概念:

  • enum: 現在想想 enum 枚舉類型非常實用,很多其它的語言都內置了這一類型,合理的使用枚舉,能讓我們的代碼可讀性更高,比如:
const enum MediaTypes {
JSON = "application/json"
}
fetch("https://swapi.co/api/people/1/", {
headers: {
Accept: MediaTypes.JSON
}
})
.then((res) => res.json())
  • never: never 代表代碼永遠不會執行到這裡,常常可以應用在 switch case 的 default中,防止我們遺漏 case 未處理,比如:
enum ShirTSize {
XS,
S,
M,
L,
XL
}
function assertNever(value: never): never {
console.log(Error(`Unexpected value '${value}'`));
}
function prettyPrint(size: ShirTSize) {
switch (size) {
case ShirTSize.S: console.log("small");
case ShirTSize.M: return "medium";

case ShirTSize.L: return "large";
case ShirTSize.XL: return "extra large";
// case ShirTSize.XS: return "extra small";
default: return assertNever(size);
}
}

下面是上述代碼在我的編輯器中的截圖,編輯器會通過報錯告知我們還有未處理的情況。

關於TypeScript , 你知道多少?

  • 類型斷言: 類型斷言其實就是你告訴編譯器,某個值具備某種類型。有兩種不同的方式可以添加類型斷言:
  • <string>someValue/<string>
  • someValue as string

關於類型斷言,我看文檔時的疑惑點在於,我想不到什麼情況下會使用它。後來發現,當你知道有這麼一個功能,在實際使用過程中,就會發現能用得著,比如說遷移遺留項目時。

  • Generics(泛型): 泛型讓我們的數據結構更為抽象可複用,因為這種抽象,也讓它有時候不是那麼好理解。泛型的應用場景非常廣泛,比如:
type Nullable = {
[P in keyof T]: T[P] | null;
};

能夠讓某一種接口的子類型都可以為 null。我記得我第一次看到泛型時也覺得它很不好理解,不過後來多用了幾次後,就覺得還好了。

  • interface 和 type interface 和 type 都可以用來定義一些複雜的類型結構,最很多情況下是通用的,最初我一直沒能理解它們二者之間區別在哪裡,後來發現,二者的區別在於:
  • interface創建了一種新的類型,而 type 僅僅是別名,是一種引用;
  • 如果 type 使用了 union operator (|) 操作符,則不能將 type implements 到 class 上;
  • 如果 type 使用了 union(|) 操作符 ,則不能被用以 extends interface
  • type 不能像 interface 那樣合併,其在作用域內唯一; [1]

在視頻 Use Types vs. Interfaces from @volkeron on @eggheadio 中,通過實例對二者的區別有更細緻的說明。

值得指出的是,TypeScript handbook 關於 type 和 interface 的區別還停留在 TS 2.0 版本,對應章節現在的描述並不準確,想要詳細瞭解,可參考 Interface vs Type alias in TypeScript 2.7這篇文章。
  • 類型保護 TS 編譯器會分析我們的程序併為某一個變量在指定的作用域來指明儘可能確切的類型,類型保護就是一種輔助確定類型的方法,下面的語句都可以用作類型保護:
  • typeof padding === "number"
  • padder instanceof SpaceRepeatingPadder

一個應用實例是結合 redux 中的 reducer 中依據不同的 type,TS 能分別出不同作用域內 action 應有的類型。

  • 類型映射 類型映射是 TypeScript 提供的從舊類型中創建新類型的一種方式。它們非常實用。比如說,我們想要快速讓某個接口中的所有屬性變為可選的,可以按照下面這樣寫:
interface Person {
name: string;
age: number;
}
type PartialPerson = { [P in keyof Person]?: Person[P] }

還有一個概念叫做 映射類型,TS 內置一些映射類型(實際上是一些語法糖),讓我們可以方便的進行類型映射。比如通過內置的映射類型 Partial ,上面的表達式可以按照下面這樣寫:

interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<person>
/<person>

常見的映射類型,可以參看這篇文章 — TS 一些工具泛型的使用及其實現,除了做為語法糖內置在 TS 中的映射類型(如Readonly),這篇文章中也提到了一些未內置最 TS 中但是很實用的映射類型(比如 Omit)。

  • 第三方的庫,如何得到類型支持 我們很難保證,第三方的庫都原生支持 TS 類型,在你使用過一段時間 TS 後,你肯定安裝過類似 @types/xxx 的類型庫,安裝類似這樣的庫,實際上就安裝了某個庫的描述文件,對於這些第三方庫的類型的定義,都存儲在DefinitelyTyped 這個倉庫中,常用的第三方庫在這裡面都有定義了。在 TypeSearch 中可以搜索第三方庫的類型定義包。

關於類型,還有一些很多其它的知識點,不過一些沒有那麼常用,一些沒有那麼難理解,在此暫不贅述。

消化學到的新概念

我首次看完《TypeScript handbook》時,確實覺得自己懂了不少,但是發現動手寫代碼,還是會經常卡住。追其原因,可能在於一下子接收了太多的新概念,一些概念並沒有來得及消化,這時候我推薦看下面這門網課:

  • Advanced Static Types in TypeScript — egghead.io

看視頻算是一種比較輕鬆的學習方式,這門課時長大概是一個小時。會把 TypeScript handbook 這本書中的一些比較重要的概念,配合實例講解一次。可以跟著教程把示例敲一次,在 vscode 中多看看給出的提示,看完之後,對 TS 的一些核心概念,肯定會有更深的理解。

模仿和實踐

想要真的掌握 TS,少不了實踐。模仿也是一種好的實踐方式,已 React + TypeScript 為例,比較推薦的模仿內容如下:

  1. TypeScript-React-Starter ,這是微軟為 TS 初學者提供的一個非常好的資料,可以繼續使用我們上面構建的 playground ,參照這個倉庫的 readme 寫一次,差不多就能知道 TS 結合 React 的基本用法了;
  2. GitHub - react-typescript-cheaTSheet,這個教程也比較簡單,不過上面那個教程更近了一步,依據其 readme 繼續改造我們的 playground 後,我們能知道,React + Redux + TypeScript 該如何配合使用;
  3. react-redux-typescript-guide ,這個教程則展示了基於 TypeScript 如何應用一些更復雜的模式,我們也可以模仿其提供的用法,將其應用到我們自己的項目中;
  4. Ultimate React Component Patterns with Typescript 2.8 ,這篇文章則可以做為上述內容的補充,其在掘金上有漢語翻譯,點贊量非常高,看完之後,差不多就能瞭解到如果使用 TS 應對各種 React 組件模式了。
  5. Use TypeScript to develop React Applications — egghead.io,隨後如果想再輕鬆一點,則可以再看看這個網課,跟著別人的講解,回頭看看自己模仿著寫的一些代碼,也許會有不同的感觸;

至此,你肯定就已經具備了基礎的 TS 開發能力,可以獨立的結合 TS 開發相對複雜的應用了。

更深的理解

當然也許你並不會滿足於會用 TS,你還想知道 TS 的工作原理是什麼。這時候推薦閱讀下面兩篇內容:

  • TypeScript Compiler Internals · TypeScript Deep Dive ,TS 編譯的核心還是 AST,這篇文章講解了 TS 編譯的五個階段( Scanner /Parser / Binder /Checker /Emitter )分別是怎麼工作的;
  • Learn how to contribute to the TypeScript compiler on GitHub through a real-world example,則是另外一篇比較好的瞭解 TS 運行原理的資料。

關於 TS 的原理,我還沒有來得及仔細去看。不過 AST 在前端中的應用還真是多,待我補充更多的相關知識後,也許會對 AST 有一個更全面的總結。

TS 當然也不是沒有缺點,The TypeScript Tax [2] 是一篇非常優秀的文章,閱讀這篇文章能讓我們更為客觀看待 TS,雖然站在作者的角度看,TS 弊大於利,主要原因是 TS 提供的功能大多都可以用其它工具配合在一定程度上代替,而且類型系統會需要寫太多額外的代碼,類型系統在一定程度上也破壞了動態語言的靈活性,讓一些動態語言特有的模式很難在其中被應用。作者最終的結論帶有很強的主觀色彩,我並不是非常認可,但是這篇文章的分析過程非常精彩,就 TS 的各種特性和現在的 JS 生態進行了對比,能讓我們對 TS 有一個更全面的瞭解,非常推薦閱讀,也許你會和我一樣,看完這個分析過程,會對 TS 更感興趣。

TS 每隔幾個月就會發佈一個新的小版本,每個小版本在 TypeScript 官方博客[3] 上都會有專門的說明,可用用作跟進學習 TS 的參考。

  • TypeScript handbook — book
  • TypeScript Deep Dive — book
  • TypeScript-React-Starter — github
  • react-typescript-cheaTSheet — github
  • Advanced Static Types in TypeScript — egghead.io
  • Use TypeScript to develop React Applications — egghead.io
  • Practical Advanced TypeScript — egghead.io
  • Ultimate React Component Patterns with Typescript 2.8 — medium

參考

  • 關於interface 和 type 二者的區別,handbook 的描述過時了,可參照 Interface vs Type alias in TypeScript 2.7
  • The TypeScript Tax 對 TS 的優缺點有非常詳細的描述,非常推薦閱讀 The TypeScript Tax
  • ts 更新時,官方博客地址可參考這裡
  • [devblogs.microsoft.com/typescript/

評論轉發有驚喜哦~


分享到:


相關文章: