類型即正義:TypeScript 從入門到實踐(四):5000字長文帶你從新認識泛型

本文由圖雀社區成員 pftom 寫做而成,歡迎加入圖雀社區,一塊兒創做精彩的免費技術教程,予力編程行業發展。javascript

歡迎閱讀 類型即正義,TypeScript 從入門到實踐系列:html

本文所涉及的源代碼都放在了 Github  或者 Gitee 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+GithubGitee倉庫加星❤️哦~前端

此教程屬於 React 前端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵咱們繼續創做出更好的教程,持續更新中~java

在以前的文章中,咱們瞭解了 TypeScript 主要分爲 JS 語言側和類型側兩個部分。node

在介紹了類型側的一些基礎知識,咱們用這些學到的基礎知識去註解對應的 JS 內容,將 JS 內容如變量、函數、類等類型化,這樣確保寫出的代碼很是利於團隊協做,且能快速排錯。react

在瞭解了以前幾篇文章裏面的知識以後,你應該可使用 TypeScript 進行正常的項目開發了。git

源起

爲何要學泛型?由於它能夠幫助你 「面向編輯器代碼提示編程」 :)github

學習準備

配置 TypeScript 環境

建立一個 node 項目:typescript

mkdir ts-study
cd ts-study && npm init -y
複製代碼

配置 TypeScript 環境:npm

npm install typescript # 安裝 TypeScript
npx tsc --init # 生成 TypeScript 配置文件
複製代碼

修改 tsconfig.json 文件,設置對應的 TS 編譯器須要編譯的文件以下:

{
  "compilerOptions": {
    "outDir": "./dist" // 設置編譯輸出的文件夾
  },
  "include": [
    // 須要編譯的ts文件一個*表示文件匹配**表示忽略文件的深度問題
    "./src/**/*.ts"
  ],
  "exclude": ["node_modules", "dist", "**/*.test.ts"] // 排除不須要編譯的文件夾
}
複製代碼

配置 TypeScript 編譯執行腳本,使用 VSCode 編輯器打開 ts-study 項目,而後修改 package.json 的 scripts 字段以下:

{
  "name": "ts-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc",
    "build:w": "tsc -w"
  },
  "author": "pftom <1043269994@qq.com>",
  "license": "MIT",
  "dependencies": {
    "typescript": "^3.7.4"
  }
}
複製代碼

接着在項目根目錄新建 src 文件夾,並在裏面新建  index.ts  文件,接着在項目根目錄下的命令行執行 npm run build:w 開始監聽 index.ts 文件的修改。

通過上面的操做,你的 VSCode 編輯器應該是以下樣子:

image.png
其中 TERMINAL 終端表示正在監聽文件修改並編譯中,當前文件的編譯結果沒有錯誤,由於咱們的 src/index.ts 裏面尚未寫任何內容。一切準備就緒,開始 「面向編輯器代碼提示編程」!✌️

從一個簡單的例子提及

先不扯那麼多泛型的概念,咱們先來看一個簡單的例子,在 src/index.ts 裏面編寫以下代碼:

function getTutureTutorialsInfo(info) {
  return info;
}
複製代碼

咱們編寫了一個獲取圖雀教程信息的函數,接收 info 輸入,而後返回 info ,即明確參數類型和返回類型同樣。如今這個還只是一個 JavaScript 函數,咱們來給它進行類型註解。

寫一個 Low 一點的 TS 函數

.... 這怎麼註解?此時正在閱讀的你可能會冒出這個疑問。

對的,這怎麼註解?咱們面臨着以下幾個難題:

  • info 類型未知,它多是 string 、 number 或者其餘類型
  • info 類型未知的狀況下,咱們還要註解返回值類型,而且此返回值類型要和 info 類型一致,因此咱們的返回值類型這裏也未知。

相信有同窗會嘗試這樣去解決:

function getTutureTutorialsInfo(info: any): any {
  return info;
}
複製代碼

很好!你成功寫了第一個 "AnyScript` 函數,這個函數和 JS 函數無異,根本沒法用到 TS 的強大類型補全機制,不信你能夠在你的 VSCode 編輯器裏面嘗試加入以下代碼:

function getTutureTutorialsInfo(info: any): any {
  console.log(info.length);
  return info;
}
複製代碼

能夠看到咱們添加了一個打印語句 console.log ,若是你沒有 Copy 上面的代碼,而是選擇手敲的話,你會發現輸入 info. 的時候,編輯器裏面沒有提示補全 length 屬性,由於給 info 註解了 any 類型,TS 編譯器沒法推斷此 info 是什麼類型,因此也沒有對應的補全,沒有補全的 TypeScript 代碼是沒有生命的😿

類型的函數?

那麼思考一下,這裏該如何作類型註解了?相信你已經有答案了,這就是咱們這一節要引出的重點:「泛型」 ,我將它稱之爲 「類型的函數」,對應 JS 的函數同樣,聲明一個 「類型變量」,而後在類型函數代碼塊裏面可使用這個 「類型變量」。

一個 JS 函數以下:

function jsFunc(varOne) {
  const res = varOne + 1;
  return res;
}
複製代碼

能夠看到一個 JS 函數,有一個 varOne 參數,這個參數變量能夠在函數體中使用。接下來咱們來看一下爲何我把泛型稱之爲 「類型的函數」,修改咱們 src/index.ts 裏面的內容:

function getTutureTutorialsInfo<T>(info: T): T {
  console.log(info.length);
  return info;
}
複製代碼

能夠看到咱們給 getTutureTutorialsInfo 後面加上 <T> 這個相似咱們上面那個 JS 函數的 (varOne) ,而後咱們在原 JS 函數參數和返回值中使用了這個 「類型變量」  T : (info: T): T ,這樣咱們就解決了上面的兩個難題:

  • 咱們定義了 T 這樣一個 「類型變量」,並用它來註解咱們的 JS 函數參數 info 和其返回值,T 既然是一個 「類型變量」,那麼接收此 「類型變量」 的 「類型的函數」 - 泛型,在以後被調用的時候,咱們能夠根據需求指定傳入的類型,好比 string 、 number 等,這就確保了調用函數的用戶來決定 info 的類型 T ,這樣參數的類型就肯定了。
  • 參數和返回值類型都使用了 T 來作類型標註,因此參數值和返回值類型一致。

可是稍等,上面的代碼在編輯器中報錯了:

image.png
由於咱們將這個函數泛型化了,明確了泛型變量 T 是一個明確類型,因此咱們以前的 info.length 會報錯,固然這裏有同窗會問了,我要是這裏 T 在以後泛型 (類型的函數)調用的時候傳入的是 string 類型,那不是就有 length 屬性了嘛,很遺憾,由於 T 還多是 number 類型,而 number 類型的變量沒有 length 屬性,因此 TS 編譯器報錯了。

爲了解決上面的問題,咱們能夠更近一步,對函數作出修改以下:

function getTutureTutorialsInfo<T>(info: T[]): T[] {
  console.log(info.length);
  return info;
}
複製代碼

這樣就好啦,不只類型肯定了,並且參數和返回值類型也一致,而且咱們還能明確的使用 info.length 了,由於 TS 編譯器知道 info 是一個數組,這個時候你在 VSCode 編輯器裏面輸入 info. ,應該會提示你以下效果:

image.png
有了代碼補全的 TS 充滿了活力🔥!

通過上面的例子,咱們發現,其實泛型,就像是一個關於 「類型的函數」 同樣,給定輸入的類型變量,而後可使用輸入變量通過組合好比 T[] 、進行聯合類型或交叉類型操做,來做爲註解類型使用。

類型函數的使用

上面咱們定義了第一個泛型 - 「類型的函數」,接下來咱們來嘗試使用咱們的泛型,在 src/index.ts 中對代碼作出對應的修改以下:

function getTutureTutorialsInfo<T>(info: T[]): T[] {
  console.log(info.length);
  return info;
}

getTutureTutorialsInfo<string>(['hello tuture', 'hello world'])
複製代碼

能夠看到對應 <T> 定義了泛型中的類型變量,咱們在調用泛型時,也對應寫了 <string> ,這樣 T 就在 getTutureTutorialsInfo 函數中就會以 string 的類型被使用,參數 info 和返回值也就對應了 string[] ,你的 VSCode 編輯器裏面調用的效果應該以下圖,將鼠標移動到 getTutureTutorialsInfo 上,會直接顯示 getTutureTutorialsInfo 函數的類型定義,能夠看到已經成功將 T 換成了 string 。

image.png

而且咱們還了解到,泛型的使用和 JS 函數的調用一脈相承,更加堅決了咱們 泛型 就是 「類型的函數」 的說法和認知。

注意:

  • 上面的泛型中使用的 T 變量,其實只是一個 TypeScript 界比較習慣性的用法,經常使用的還有 U 等,固然你也能夠寫成 YourT ,這裏不限制。
  • 上面的泛型調用時,T 接受的類型能夠是任意類型,好比對象、函數等類型,不只僅限於 string 、 number 等

泛型,再回顧

咱們在上面用了不少的筆墨來試圖將泛型和 「類型的函數」 劃上等號,目的是爲了讓你理解泛型它工做的一個原本面貌。瞭解了泛型原本面貌以後,相信諸如使用泛型可使得 TS 代碼組件化,複用代碼,你也能瞭然如胸了。

泛型是在調用時再限定類型

咱們在定義泛型的時候,是一系列類型變量,如 T 、 U 等,這些變量實際的類型咱們在定義的時候是不知道的,只有在進行泛型調用的時候,由用戶給定實際的類型,因此這裏有一種延遲聲明類型的做用。

泛型是否也有多個類型變量?

那麼,既然泛型能夠看作是 「類型的函數」,那麼函數能接收多個參數的話,咱們的泛型也能接收多個類型變量,好比:

function getTutureTutorialsInfo<T, U>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

getTutureTutorialsInfo<string, object>(['hello tuture'], { username: 'tuture'})
複製代碼

能夠看到,咱們修改了 getTutureTutorialsInfo 函數的泛型定義,添加了一個新的類型變量 U ,並用 U 來註解了函數的第二個參數 profile 的類型。

一樣,在調用 getTutureTutorialsInfo 函數的時候,咱們也須要傳入兩個類型變量,這裏咱們的 profile 被認爲是一個 object 類型。

匿名函數泛型?

在以前的內容中,咱們經過命名函數來說解了泛型,那麼匿名函數如何使用泛型了?其實和命名函數相似,只不過匿名函數是以下形式:

const getTutureTutorialsInfo: <T>(info: T[]) => T[] = (info) => {
  console.log(info.length);
  return info;
}

// 或者
const getTutureTutorialsInfo: <T>(info: T[]) => T[] = function (info) {
  console.log(info.length);
  return info;
}
複製代碼

咱們直接給匿名函數被賦值的變量進行匿名函數的註解,並加上泛型,你應該回想起以前給一個變量註解函數類型時的樣子:

(args1: type1, args2: type2, ..., args3: type3) => returnType
複製代碼

而匿名函數泛型只不過在以前加上了 <T> 類型變量,而後能夠用於註解參數和返回值。

泛型默認類型參數?

既然咱們聲稱泛型是關於 「類型的函數」,爲了更加深入的論證咱們這個觀點,咱們再進一步。

咱們都知道函數存在默認參數一說,那麼做爲 「類型的函數」 - 泛型,是否也有默認類型參數這一說了?很差意思,還真的有!咱們來看個例子:

function getTutureTutorialsInfo<T, U = number>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

getTutureTutorialsInfo<string, string>(['hello world'], 'hello tuture')
複製代碼

能夠看到咱們給類型變量 U 一個默認的類型參數 number (還記得 ES6 裏面有默認值的參數必須靠後放置嘛?)

以後咱們在進行泛型調用的時候,卻給 U 傳了 string 類型,把這段代碼放到 src/index.ts 裏面,應該不會報錯,而且編輯器裏面有良好的提示:

image.png

泛型,繼續前進

接下來咱們繼續深刻泛型,解答以前文章裏的一些疑問,好比:

  • 泛型數組
  • 類泛型

同時咱們還會了解一些新的概念,好比:

  • 接口泛型
  • 類型別名泛型
  • 泛型約束

解決遺留的問題

泛型數組

這個咱們已經在上面的例子中用到了,泛型實際上定義了一系列類型變量,而後咱們能夠對這些類型變量作任意的組合以適應各類不一樣的類型註解需求,其中一個組合例子就是泛型數組 - 某個類型變量的數組形態,也就是咱們上面提到的 info: T[] ,其中 T[] 就是泛型數組。

固然泛型數組的表達形式還有另一種:

Array<T> 
複製代碼

即以泛型調用的形式返回一個關於泛型變量 T 的數組類型。因此咱們的 getTutureTutorialsInfo 函數能夠寫成以下樣子:

function getTutureTutorialsInfo<T>(info: Array<T>): Array<T> {
  console.log(info.length);
  return info;
}

getTutureTutorialsInfo<string>(['hello tuture', 'hello world'])
複製代碼

類泛型

類泛型的形式和函數泛型相似,咱們來看一個類泛型的定義的調用,在 src/index.ts 裏面額外添加下面的內容:

// 上面是 getTutureTutorialsInfo 泛型函數的定義和調用

class Tuture<T> {
 	info: T[];
}

let tutorial = new Tuture<string>()
tutorial.info = ['hello world', 'hello tuture'];
複製代碼

類泛型的定義也是在類名以後添加 <T> 這樣的形式,而後就能夠在類中使用 T 類型變量來註解類型。而類泛型的調用和函數泛型的調用相似。

學習了類泛型,咱們再來解析一下在上一篇文章中提到的那個 TodoInput 組件,相似下面這樣:

class TodoInput extends React.Component<TodoInputProps, TodoInputState> {
  // ... 組件內容 
}
複製代碼

這個實際上分爲兩個部分,首先是 React.Component 組件基類的類泛型調用,而後是 TodoInput 集成自這個類泛型。由於派生類 TodoInput 能夠獲取到父類的屬性和方法,因此在 TodoInput 中使用的 this.props 和 this.state 在被類型註解以後,就能夠在編碼時自動補全,你在寫代碼的時候應該能夠享受到以下好處:

image.png

開啓新篇章

瞭解了函數泛型、類泛型,你有可能有一點想法了關於泛型,是否是咱們以前的不少講解過的內容,如類型別名、接口等。你想對了!TS 會在儘量多的地方,能用泛型就用上泛型,由於泛型能夠將代碼組件化,方便複用,全部智能的編譯器,能不讓你多寫的東西,就絕對不會讓你多寫,統統用泛型給整上。

接口泛型

在瞭解接口泛型以前,咱們先來看一個接口是怎麼寫的,在 src/index.ts 裏面添加以下代碼:

interface Profile {
  username: string;
  nickName: string;
  avatar: string;
  age: string;
}
複製代碼

通常咱們的 Profile 相似上面的內容,可是有時候有些字段會根據需求的不一樣而不一樣,好比 age 這個字段,有些人喜歡定義成數字類型 number ,有些人喜歡定義成字符串類型 string ,因此這又是一個延遲賦予類型的例子,能夠藉助泛型來解決,咱們修改一下上面的代碼:

 interface Profile<T> {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

type ProfileWithAge = Profile<string>
複製代碼

能夠看到,接口泛型的聲明和調用與函數、類泛型的相似,它容許你在接口裏面定義一些屬性,使用類型變量來註解,在調用時指明這個屬性的類型。

類型別名泛型

由於在不少場景下,類型別名和接口充當相似的角色,因此在瞭解完接口泛型以後,咱們有必要來了解學習一下類型別名如何結合泛型使用,和接口相似,將上面的接口泛型 Profile 用類型別名重寫以下:

type Profile<T> = {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

type ProfileWithAge = Profile<string>
複製代碼

能夠看到,基本一致!

泛型約束

咱們來解決以前的一個遺留問題,那就是即便我使用了泛型,我仍是不知道某個被泛型的類型變量註解的變量的一個結構是怎麼樣的即:

function getTutureTutorialsInfo<T, U>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

getTutureTutorialsInfo<string, object>(['hello tuture'], { username: 'tuture'})
複製代碼

上面咱們用類型變量 U 註解了 profile 參數,但咱們在使用 profile 的時候,依然不知道它是什麼類型,也就是說泛型雖然解決了類型的可複用性,可是仍是不能讓咱們寫代碼時得到自動補全的能力😭

重申:沒有補全的 TypeScript 代碼是沒有生命的!

那麼咱們如何讓在既使用泛型的同時,還能得到代碼補全了?答案相信你也猜到了, 那就是咱們這一節要講的泛型約束。 咱們修改 src/index.ts  裏面的代碼以下:

type Profile<T> = {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

function getTutureTutorialsInfo<T, U extends Profile<string>>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

複製代碼

能夠看到,咱們複用了以前定義的 getTutureTutorialsInfo 和 Profile ,可是在 getTutureTutorialsInfo 泛型中第二個類型變量作了點改進,以前只是單純的 U ,如今是 U extends Profile<string> , Profile<string> 表示調用類型別名泛型生成一個 age 爲 string 的新類型別名,而後經過 U extends ... 的方式,用 Profile<string> 來限制 U 的類型,也就是 U 必須至少包含 Profile<string> 的類型。

這個時候,咱們在 VSCode 編輯器裏面嘗試輸入 profile. ,應該能夠神奇的發現,有了自動補全:

image.png
而且還能瞭解到 age 是 string 屬性!

再次!有了代碼補全的 TS 充滿了活力🔥!

固然這裏的用於約束的 Profile<string> 能夠是一個類型別名,也能夠是一個接口,也能夠是一個類:

class Profile<T>  {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

// 或者
interface Profile<T>  {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

// 或者
type Profile<T> = {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}
複製代碼

更近一步,這裏的用於約束類型變量的類型能夠是一些更加高級的類型如聯合類型、交叉類型等:

type Profile<T> = {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

type Tuture = {
	github: string;
  remote: string[];
}

function getTutureTutorialsInfo<T, U extends Profile<string> & Tuture>(info: T[], profile: U): T[] {
  console.log(info.length);
  console.log(profile);
  return info;
}

複製代碼

能夠看到咱們使用了 Profile<string> 和 Tuture 的交叉類型來約束 U ,在咱們的 VSCode 編輯器裏面應該會有以下補全效果:

image.png

深刻實踐,註解構造函數

在瞭解泛型的基礎知識,而且結合函數、接口、類型別名和類進行結合使用以後,相信你對如何使用泛型已經有了一點經驗了。

而瞭解了泛型,你就能夠開始嘗試深刻 TS 類型編程的世界了!接下來咱們開始深刻一下高階的 TS 類型編程知識,並嘗試講解一些比較邊緣狀況如何進行類型註解。

咱們須要一個 createInstance 函數,它接收一個類構造函數,而後返回此類的實例,並能在調用以後得到良好的代碼補全提示(!很重要),而且此函數還須要有足夠好的通用性能處理任意構造函數(!泛型) 。咱們嘗試在 src/index.ts  裏面編寫一個類以及一個建立此類實例的方法:

class Profile<T> {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

class TutureProfile extends Profile<string> {
	github: string; 
  remote: string[];
}

function createInstance(B) {
  return new B();
}

const myTutureProfile = createInstance(TutureProfile);
複製代碼

不要問我爲何 createInstance 的參數是 B ,由於咱們最後很 new B() 。😁

當咱們編寫了上面這個 createInstance 時,當咱們嘗試在調用以後輸入 . : createInstance(TutureProfile). ,發現編輯器裏面沒有補全提示實例化對象的相關屬性如 username 等

image.png
首先咱們來解析一下構造函數的樣子,由於 TS 類型是鴨子類型,是基於代碼的實際樣子來進行類型註解的。構造函數是可被實例化的函數,便可以經過 new XXX() 進行調用來建立一個實例,因此構造函數的註解應該相似這樣:

interface ConstructorFunction<C> {
 	 new (): C;
}
複製代碼

即形如 new (): C 的函數形式,表示能夠經過調用 new XXX() 生成一個 XXX 的實例。即某個類:

class Profile<T> {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}
複製代碼

咱們註解其構造函數相似下面:

const profileConstructor: ConstructorFunction<Profile<string>> = Profile;
複製代碼

這裏有同窗還記得嘛,咱們在上一篇文章中講到一個類在聲明的時候會聲明兩個東西:1)用於註解此類實例的類型 2)以及此類的構造函數。這個例子是用來表達類在聲明時聲明的這兩樣東西的最佳例子之一即:

  • ConstructorFunction 接口泛型接收的 C 用來註解 new () 生成的實例,此爲第一:用於註解此類實例的類型。
  • 用於註解 Profile 的構造函數的類型 ConstructorFunction<Profile<string>> ,在註解 profileConstructor 變量以後,其初始化賦值是 Profile 自己,而且你能夠在你的 VSCode 編輯器裏面編寫上面的代碼,應該不會報錯,這說明了第二:聲明瞭此類的構造函數。

瞭解了構造函數如何進行類型註解以後,咱們來完成第三點要求,讓這個 createInstance 更具通用性,二話不說,泛型走起!最終代碼以下:

class Profile<T> {
  username: string;
  nickName: string;
  avatar: string;
  age: T;
}

class TutureProfile extends Profile<string> {
	github: string; 
  remote: string[];
}

interface ConstructorFunction<C> {
 	 new (): C;
}


function createInstance<A extends Profile<string>>(B: ConstructorFunction<A>) {
  return new B();
}

const myTutureProfile = createInstance(TutureProfile);
複製代碼

如今你在 VSCode 編輯器 createInstance(TutureProfile) 後面輸入 . 應該能夠看到代碼補全:

image.png
這個例子其實關於 extends 類型約束那一塊有點多餘,可是爲了組合咱們在這一篇裏面學到的知識,因此我額外把它也加上了,能夠看到咱們重拾了全部的代碼補全,代碼補全🐂🍺

上面類中如 remote 等屬性會有紅色下劃線是由於報了 Property 'remote' has no initializer and is not definitely assigned in the constructor.ts(2564) ,字面意思就是沒有初始化這些屬性,這個不重要,能夠經過配置移除,也能夠初始化。It's your choice!

參考資料

想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。

相關文章
相關標籤/搜索