TypeScript - 一種思維方式

Fundebug發表於2019-05-11

摘要: 學會TS思考方式。

Fundebug經授權轉載,版權歸原作者所有。

電影《降臨》中有一個觀點,語言會影響人的思維方式,對於前端工程師來說,使用 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 會有下面這類提醒:

2019-05-11-002

不用去查文件,我們就能看到其提供的 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 的過程中,主要參考了以下這些資料,你可以直接點選連結檢視,也可以繼續看後文,我對這些資料有著一些簡單的分析。

在閱讀上述資料的過程中,我使用 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 casedefault中,防止我們遺漏 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
  • someValue as string

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

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

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

  • interface 和 type interfacetype 都可以用來定義一些複雜的型別結構,最很多情況下是通用的,最初我一直沒能理解它們二者之間區別在哪裡,後來發現,二者的區別在於:
    • 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>
複製程式碼

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

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

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

消化學到的新概念

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

看視訊算是一種比較輕鬆的學習方式,這門課時長大概是一個小時。會把 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 的工作原理是什麼。這時候推薦閱讀下面兩篇內容:

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

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

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

推薦閱讀

下述參考內容在文中,都有連結,如果都看過,則無需再重複檢視了。

參考

相關文章