一次就搞懂泛型使用

泛型主要是讓程式的變化性提高,可以讓整個程式碼的重複使用變得更好,不會讓原本只需要寫一次的東西,因為加入型態之後,而變成需要不斷重複寫,這樣嚴謹的型態規範,就會變得礙手礙腳,所以使用泛型來解決這些問題

泛型

在之前篇學到function的時候,有稍微提到泛型的用法,簡單複習一下,使用泛型就是在宣告的時候先給一個不確定的型態,並使用”<>”包起來,在真的使用的時候再賦予型態

在function的部分,可以同時給予參數,回傳值設定泛型,依照範例,需要設定兩種型態(T、U)給他,並回傳第一種型態T

function nowPrint<T, U>(test: T, test2: U): T {
  return test;
}

nowPrint<string, number>("字", 22);
nowPrint<number, boolean>(11, false);

interface的泛型

interface在建立的時候,也需要給予型態,但如果型態不確定呢,那就可以使用泛型,當被實作的時候,就可以給予確定的值

像這樣,name是一般的型態,而age想要給予不同型態但不確定,先給泛型,接下來分別透過,funciton跟class實作看看

interface Animal<T> {
  name: string;
  age: T;
}

function實作interface的泛型

建立一個function來實作剛剛的Animal,所以可以看到,這邊只要在function設定好泛型,並給予interface來實作,就不會報錯,並在使用的時候,給予正期望的型態跟正確的參數

function dog<T>(age: T): Animal<T> {
  return {
    name: "hank",
    age: age,
  };
}
dog<number>(10);

通常會整理成這樣,剛那是寫給我自己看,怕忘記是要回傳物件回去

function dog<T>(age: T): Animal<T> {
  let data: Animal<T> = {
    name: "hank",
    age: age,
  };
  return data;
}

dog<number>(10);

class實作interface的泛型

其實跟function差不多,在實作時候,從class這邊設定一個泛型給interface,並在建立(new)的時候給予需要的型態即可,只是目前這樣,age只是空的,可以給建構子,或是另外賦予值

class Cat<T> implements Animal<T> {
  name: "";
  age: T;
}

let cat = new Cat<string>();

直接給建構子,初始化

class Cat<T> implements Animal<T> {
  constructor(age: T) {
    this.age = age;
  }
  name: "";
  age: T;
}
let cat = new Cat<string>("10");

或是另外賦予值

let cat = new Cat<string>("10");
cat.age = "12";

深入了解extends

之前看到的extends用法,就是用來繼承,interface繼承interface,或是class繼承class,那其實還有很多複雜用法,一個一個來搞懂他

extends基本用法

以下簡單的範例interface跟class繼承部分,那麼可以看到子類別可以使用父類別的東西,而interface也必須建立父層的屬性,基本的繼承用法

/*....interface.... */
interface Animal {
  name: string;
}
interface Dog extends Animal {
  age: number;
}

let dog: Dog = {
  name: "",
  age: 11,
};
/*....class.... */
class Animal {
  name: string;
}
class Dog extends Animal {
  age: number;
}
let dog1 = new Dog();
dog1.age;
dog1.name;

extends條件式判斷

學到這邊的時候,我開始驚呆了,居然有這種用法,簡單整理幾個判斷依據,我們先看一下他設定的樣子

//左子右父
type T1 = string extends string ? string : number;

首先要把這東西拆解成if-else,那extends就有點像是”==”的概念,只是他這邊涵蓋為兼容,所以以上面來說,右邊string是完全可以被左邊string兼容,所以結果是true,最後得到?旁的string

那在來是兩個狀況來看就更清楚了,我們要以右邊為主

//T1 = string
type T1 = string extends string | number ? string : number;
//T2 = number
type T2 = string | number extends string ? string : number;

再來沿用上面的class來看,就能清楚發現,因為Dog繼承了Animal,所以Animal涵蓋了Dog,最終結果T3等於string,那如果倒過來,T4就變成number了(就不附圖了)

class Animal {}
class Dog extends Animal {
  name: string;
}
//T3=string
type T3 = Dog extends Animal ? string : number;
//T4=number
type T4 = Animal extends Dog ? string : number;

那介面繼承關係,也跟上面class結果一樣,但是不同關係的怎麼判斷。有趣的是,Cat的東西在Dog裡面有,所以Cat在右邊完全被Dog兼容,所以T5就是string

interface Cat {
  name: string;
}
interface Dog {
  name: string;
  age: number;
}
//T5=string
type T5 = Dog extends Cat ? string : number;
//T6=number
type T6 = Cat extends Dog ? string : number;

泛型加上extends

可怕的東西來了,除了各自會使用之外,還需要合併來使用,這是為了讓我們有判斷型態的依據之外,還要有更多型態加入一起來判斷,直接看幾個範例來分析

※泛型被丟進去的時候,會被當作union來看,先大概知道這個觀念就好

  1. T7的部分很明顯,就是完全兩個相同的東西,所以T7是string
  2. T8、T9即使你在哪一邊,就是不一樣的東西,至時候把extend當作 “==” 就很好理解
  3. 接下來加入泛型,也就是我們可以在使用他的時候,就可以加入自己想要的類型進來,其實T11就是跟T7一樣的結果
  4. 接下來是T12也可以把泛型移動到結果那邊,所以T13就是h
//T7 = string
type T7 = "hank" extends "hank" ? string : number;

//T8、T9 = number
type T8 = "h" extends "hank" ? string : number;
type T9 = "hank" extends "h" ? string : number;

//T11 = string
type T10<T> = T extends "hank" ? string : number;
let T11: T10<"hank">;

//T13 = 'h'
type T12<T> = T extends "hank" ? string : T;
let T13: T12<"h">;

union泛型加上extends

更複雜的東西來了,接下來的東西,我都不太記的住,所以我作筆記回顧很重要,而union被丟進去的時候,就是會被一個一個比對

這地方很奇怪,乍看之下T14跟T16是一樣的結果,但實際是上,T15接收到泛型的時候,會被一個一個擺進去比對,所以第一次比對string extends string得到string,第二次比對number extends string的到number,所以結果是string | number

// T14 = number
type T14 = string | number extends string ? string : number;

//T15 =string | number
type T15<T> = T extends string ? string : number;
let T16: T15<string | number>;

never泛型加上extends

這邊要套用到兩個觀念

  1. never會幫當作任何類型的子類別
  2. 泛型丟never就是空值

從上述兩個觀念來看,never直接被進去比對,他可以是任意的子類別,所以符合各種型態,never甚至可以是undefined跟null的子類別。但是這邊又一個特例,當never被當作泛型丟進去,那被當作union一個一個比對的時候,never本身不帶任何回傳,所以最終結果就是never

// //T17 = string
type T17 = never extends string ? string : number;

// //T19 = never
type T18<T> = T extends string ? string : number;
let T19: T18<never>;

如何讓泛型不用union比對

從上述所有結果都知道,當泛型丟進去比對,就會是用union比對,但是我們並不想得到union的結果,只想全部來比對,這時候就可以使用 “[ ]”

還記得剛剛T14跟T15的結果嗎,兩者因為泛型的關係,看似一樣的東西卻得不同結果,那這時候只需要使用“[ ]”,兩者就會變成一樣的東西了

T20加入“[ ]”後,等於整包的東西跟string去做比對,那得到的結果就是跟T14一樣了

//T14 = number
type T14 = string | number extends string ? string : number;
//T15 =string | number
type T15<T> = T extends string ? string : number;
let T16: T15<string | number>;

//T21 = number
type T20<T> = [T] extends [string] ? string : number;
let T21: T20<string | number>;

泛型使用陣列

當我們要使用陣列的時候,我們必須要讓TS知道我們將會使用陣列,不然他會報錯

其實a也沒寫錯,只是他在屬性檢查的時候,會認為他是沒有屬性的狀態,也不確定接下來是不是會傳陣列當泛型,所以會提前報錯

那b的部分就很清楚知道,已經是繼承陣列了,所以接下來只要接收泛型,都可以使用陣列屬性

//使用陣列屬性會報錯
function a<T>(a: T) {
  console.log(a.length);
}
//可以正常使用陣列屬性
function b<T extends Array<T>>(b: T) {
  console.log(b.length);
}

infer的使用

這就像是宣告,但是是有條件的宣告,就是當 extends這個判斷成立的時候,就會判斷犯型裡面的東西,並且提出來宣告

以範例來說,當T是陣列的時候就成立了,所以

  1. TT1不是陣列,直接取後面never
  2. TT2泛型是陣列,所以宣告P是1,TT2就是1
  3. TT3泛型是陣列,所以宣告P是1 | “”,TT3就是union值為1 | “”
type TT<T> = T extends Array<infer P> ? P : never;
//TT1 = never
let TT1: TT<"1">;
//TT2 = 1
let TT2: TT<[1]>;
//TT3 = 1 | "" (union)
let TT3: TT<[1, ""]>;

infer加入function

其實就是判斷array改為判斷是不是fuction

從範例來看,就是泛型判斷是不是function,如果是的話,宣告參數帶來的型態,不是的話就是never

  1. TT5帶的泛型是function,參數型態為string,所以符合TT4的設定,選擇宣告P為型態回傳
type TT4<T> = T extends (c: infer P) => any ? P : never;
//TT5 = string
type TT5 = TT4<(c: string) => void>;

那再加入interface,其實只要會之前的判斷,就越來越清楚怎麼看這個,先看是否成立extends,在看成立後,傳來的參數型態,這次傳的P是interface TT6的型態,成立後回傳,所以TT7型態是TT6

type TT4<T> = T extends (c: infer P) => any ? P : never;
interface TT6 {
  name: string;
}
//TT7 = TT6
type TT7 = TT4<(c: TT6) => {}>;

keyof的使用

可以抓到物件內Key值,將此設為型態的話,就會變成只能使用該物件內的key值,以範例來說,就是只能使用name跟age

interface Animal {
  name: string;
  age: number;
}

type TTT = keyof Animal;
const Dog: TTT = "name";

寫入到function並且用並用泛型來控制他,一般來說,我們要抓物件中value值,寫成一個function會是這樣

我我設定一個function,那參數分別為物件,型態為泛型T、key值,型態為K,那我要retrun T[K],但這邊會報錯,因為TS並不知道什麼是T跟K

function getValue<T, K>(obj: T, key: K): T[K] {
  return obj[key];
}

所以要使用extends搭配keyof,這樣的話,就能清楚告訴TS,我們要使用K是來自於T物件中,的key值,一個寫法直接認定兩個泛型型態

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

總結

今天也是繼續學習布魯斯的 TypeScript 入門攻略|輕鬆打造實時聊天室這堂課,那說實在泛型這觀念,對於只有接觸過弱語言的我來說,並不是非常容易懂的一個觀念,畢竟再寫JavaScript的時候,並沒有這麼多觀念再強調型態的判定,而泛型的觀念都是從型態衍生出來規範程式的,所以並不是這麼簡單上手

但透過布魯斯的講解來說,很清楚的將需要用到的方法,都依照比較能理解的先後順序依依的分類好,並且作範例實際來說明這些方法怎麼去宣告,如果是自己學習的話,光看網路上的文件,只能知道語法,不知道背後的涵義,那學起來會是相當痛苦的,所以再這堂課布魯斯的 TypeScript 入門攻略|輕鬆打造實時聊天室中泛型這章節,讓我節省不少的學習成本

By dong

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。