深刻 Typescript 類型系統

最近項目中準備推廣接入 Typescript,抽空複習了一波相關的技術知識。說複習是由於以前看過了然而如今已經忘得一乾二淨了,除了不經常使用的緣由外,也是由於 Typescript 知識相對比較零散,學習時難成體系,因此趁這個機會整理整理,就當學習筆記吧。html

前言

Typescript (簡稱 TS)是 Javascript 的一個超集,它提供一套全面的對語法定義、使用的約束,來強化 JS 代碼的可讀性和可維護性,其中最重要的就是 TS 的類型系統。TS 類型系統提供了一套完備的聲明和使用類型的方案,靈活但也複雜難記,致使 TS 的學習成本較高。前端

學習 TS 的類型系統,首先要記住很重要的一點:類型系統只在編譯時起做用,最終必然不會出如今你的業務代碼裏。認識到這一點是很必要的,由於咱們學習 TS 很容易將它跟 JS 的語法代碼放在一塊來理解,其實這是不對的, TS 類型系統僅僅就是 TS 對 JS 的一種強化手段,學習 TS 類型系統,首先就是要忘記 JS,要把類型系統跟 JS 儘量劃分開來,才能避免一些語法的理解干擾。數組

讓類型更加靈活通用,是類型系統中一切語法特性的目標。獨立地理解 TS 類型系統,也是基於這個目標由淺到深地瞭解。微信

從最簡單的類型講起

聲明類型是使用類型的前提,TS 提供了許多用於聲明類型的語法特性,其惟一目的就是不斷完善和強化 TS 聲明類型的能力,讓類型聲明更加地靈活、可複用。函數

咱們從最簡單的類型開始來認識 TS,包括基礎類型和高級類型。在 TS 中要聲明一個類型,最簡單的就是使用 type 關鍵字。工具

基礎類型

基礎類型對應了 JS 的多個基礎的數據類型,主要包括:post

  • 原始類型:booleannumberstringvoidanynullundefined 以及 symbol
  • 引用類型:對象 Object 、函數 Function 以及數組 Array 等。

這是 TS 中最簡單的數據類型了,咱們列舉幾個常常用到的:學習

// 聲明的時候:
type _string = string;  // 字符串類型
type _number = number;  // 數字類型
type _boolean = boolean;  // 布爾值類型
type _any = any;  // 任意值
type _object = {  // 對象類型
  name: string;
  age: number
}
type _array = _object[];  // 數組類型
type _function = (user: _object) => number;  // 函數類型

// 使用的時候:
const str: _string = '111';
const num: _number = 3;
const show: _boolean = true;
let err: _any = -1;
err = 'something wrong';
const user: _object = {
  name: 'hankle',
  age: 23
}
const list: _array = [user];
const getAge: _function = (user) => {
  return user.age
}
複製代碼

咱們聲明瞭幾個不一樣的類型,並在使用的時候以 : [type] 的方式接入,進而對咱們的變量作了一層類型約束。從上邊例子咱們也能夠看出,type 能夠給已有類型聲明一個別名。ui

高級類型

高級類型,可讓咱們更加靈活地重用已有的類型,是複用的一種有效手段。this

聯合類型

聯合類型表示取值能夠爲多種類型中的其中一種,使用 | 鏈接符,做用機制相似於 "或"。

type keyType = string | number;

const strKey: keyType = 'key';
const numKey: keyType = 1;
複製代碼

鍵類型能夠是 string,也能夠是 number。

交叉類型

交叉類型表示變量應該具有多個類型的全部特性,使用 & 鏈接符,做用機制相似於 "並",多用於對象類型。在咱們須要對一些已定義好的 base 對象類型進行擴展時,可使用交叉類型來聲明。

type User1 = {
  name: string;
  age: number
};
type User2 = {
  sex: number
}

const user: User1 & User2 = {
  name: 'hankle',
  age: 23,
  sex: 0
};
複製代碼

_User1 & _User2 表示變量須要具有 _User1 和 _User2 中的全部屬性。

字面量類型

type 聲明的類型還能夠是一個字面量,表示這個類型的取值只能是這個固定值,這就是字面量類型:

type coco = 'coco';
const str: coco = 'coco coco';
// 不能將類型「"coco coco"」分配給類型「"coco"」。
複製代碼

有一種經常使用的字面量類型,叫「字符串字面量類型」,它結合聯合類型進行使用,用來約束取值只能是某幾個字符串中的一個:

type Key = 'name' | 'age';
type User = {
  name: string,
  age: number
}

const user: User = { name: 'hankle', age: 23 }
const getVal = (user: User, key: Key): any => user[key]

getVal(user, 'sex');
// 類型「"sex"」的參數不能賦給類型「Key」的參數。
複製代碼

key 類型限制了對於 user 對象可取的屬性鍵值,避免代碼企圖獲取 user 中並不存在的屬性。

接口:聲明類型的另外一種方式

除了 type,在 TS 中還有另一種聲明類型的方式,那就是接口(Interface)。使用 interface 關鍵字:

/** * 聲明對象 * 等同於: * type User = { name: string; age: number } */
interface User {
  name: string;
  age: number
}

/** * 聲明數組 * 等同於: * type List = User[] */
interface List {
  [index: number]: User
}

/** * 聲明函數 * 等同於: * type findFunc = (list: List, name: string) => User */
interface findFunc {
  (list: List, name: string): User
}
複製代碼

爲何已經有了 type,還須要另外創造出「接口」來聲明類型呢?事實上在不少 OOP 語言(好比 Java)中,接口是一個很重要的概念,用於對類(class)行爲進行抽象。所以,TS 的接口除了描述對象類型、數組類型或函數類型的形狀外,還能夠描述類的形狀。經過實現(implements)和繼承(extends),接口定義的類型能夠實現高度複用,這本就是 TS 類型系統的初衷。

在 TS 中,類(class)類型接口的聲明和實現是這樣的:

// 定義了一個報警類接口,要求實現該接口的類都必須擁有 alert 方法
interface Alarm {
    alert();
}
// SecurityDoor 類實現了該接口,並實現了 alert 方法
class SecurityDoor implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}
// Car 類沒有實現 alert 方法,報錯了
class Car implements Alarm {
}
// 類「Car」錯誤實現接口「Alarm」。
// Property 'alert' is missing in type 'Car' but required in type 'Alarm'.
複製代碼

type 關鍵字聲明的類型能夠經過聯合類型對類型進行擴展,而接口則是利用繼承(extends)來實現的:

interface User1 {
  name: string;
  age: number
};
// 聲明瞭接口 User2,繼承自接口 User1,它須要包含 User1 的形狀
interface User2 extends User1 {
  sex: number
}

const userq: User2 = {
  name: 'hankle',
  sex: 0
};
// Property 'age' is missing in type '{ name: string; sex: number; }' but required in type 'User2'.
複製代碼

至於說何時使用 type,何時使用 interface ,網上卻是有很多關於這個問題的解讀。其實這兩個能作的事情都大同小異,至於選用哪一個,我認爲在平時業務場景開發中,若是可能是對一些函數結構、象結構、類結構做類型約束的話,使用 interface 會更加地語義化,建議多用 interface 來聲明類型。

泛型:讓你的類型更通用

若是現有一個 find 函數,做用等同於 Array.find,須要咱們本身聲明它的函數類型,那到目前來講,咱們可能只能利用函數的聲明重載來這樣寫:

// 聲明重載
function find(list: string[], func: (item: string) => boolean): string | null;
function find(list: number[], func: (item: number) => boolean): number | null;
function find(list: object[], func: (item: object) => boolean): object | null;
// 實現
function find(list, func) {
  return list.find(func)
}
複製代碼

函數重載容許經過屢次聲明函數的不一樣形狀來實現函數的聯合類型。find 方法容許傳入多種類型的數組,其返回值也將由傳入數組所包含的元素類型決定。從上邊能夠看出,咱們重複聲明的只是不一樣的元素類型,函數的參結構是沒有變化的,因此出現了不少重複代碼。

函數的泛型

泛型(Generics)正是爲了解決這個問題。顧名思義,泛型表明不肯定的類型,它容許複雜類型在聲明的時候內部保留不肯定的類型,等到使用的時候再具體指定。泛型至關於一個函數,具體說是類型的工廠函數,既然是函數那就是要「傳參」的,泛型傳參使用<>實現。上邊例子用泛型實現是這樣的:

// find 的類型是一個泛型
function find<T>(list: T[], func: (item: T) => boolean): T | null {
  return list.find(func)
}

// 使用時再指定具體類型
find<number>([1,2,3], item => item > 2)
複製代碼

固然,你也能夠用接口來聲明函數類型:

// find 的類型 FindType 是一個泛型
interface FindType {
  <T>(list: T[], func: (item: T) => boolean): T | null } const find: FindType = function (list, func) { return list.find(func) } // 使用時指定具體類型 find<number>([1,2,3], item => item > 2) 複製代碼

泛型還能夠容許指定多個類型參數:

// tuple元素互換
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

swap<number, string>([7, 'seven']); // ['seven', 7]
複製代碼

類的泛型

泛型不只能夠做用在函數類型上,還廣泛地應用在類的類型定義中。和函數調用時再具體指定類型類似,類的泛型是在實例化對象時才具體指定的:

// 聲明瞭一個泛型類
class EleList<T> {
  list: T[]
  add: (item: T) => number
}

// 實例化時,指定爲字符串類型
const users = new EleList<string>();
users.list = ['hankle', 'nancy'];
users.add = function (item) {
  this.list.push(item);
  return this.list.length;
}
複製代碼

泛型約束

泛型容許咱們預設和使用一個不肯定的類型,但有時候「不肯定」也對咱們的使用形成了影響,好比咱們有這樣一個用於獲取長度的工具方法,入參能夠是一個數組,也能夠是一個類數組對象:

function get<T>(arg: T): number {
  return arg.length; // 類型「T」上不存在屬性「length」。
}
複製代碼

然而咱們發現它報錯了。類型 T 在這裏是任意類型,咱們並不能保證使用時指定的類型都存在屬性 length。咱們但願T是有限制的,它只能是數組或類數組對象,或者更具體地說,它必須包含 length 屬性。因此這時候,泛型約束派上用場了:

// 聲明 LengthType 類型,必須包含 length 屬性
interface LengthType {
  length: number;
}

// T 繼承自 LengthType 類型,也就代表它必須具備 length 屬性
function get<T extends LengthType>(arg: T): number {
  return arg.length;
}
複製代碼

這裏咱們聲明瞭一個具備 length 屬性的類型,而後讓類型 T 繼承該類型,從而對泛型的結構進行約束。咱們說過接口繼承也使用了 extends 關鍵字,它的深層含義就是繼承者必須包含被繼承者的全部特性,因此泛型約束一樣也是藉助 extends 的這個做用來限制泛型的結構形狀。泛型約束能夠幫助咱們減小許多無心義的判斷邏輯,在實際開發過程當中,咱們的泛型每每都不是徹底的任意類型,所以應當善於使用泛型約束。

高階類型:泛型還能這麼玩

泛型能夠是 TS 類型系統最值得利用的特性了。前邊說過,泛型是一個類型的工廠函數,換句話說,利用泛型能夠進一步構建出各式各樣的類型。TS 官方把這類類型一樣稱爲高級類型,而我認爲把它們叫作高階類型,更爲合適。

映射類型

映射類型就是一種經常使用的泛型結構。映射類型能對一箇舊類型的各項屬性進行映射,進而生成一個新類型。TS 標準庫已經內置了多個十分實用的映射類型,包括 PartialRequiredReadonlyPickRecord等,咱們挑選幾個解釋一下。

Partial<T>

type Partial<T> = {
    [P in keyof T]?: T[P];
};
複製代碼

keyof 返回的是 T 中全部屬性鍵名組成的字符串字面量類型,因此 Partial 用於把傳入類型 T 中的各項屬性轉化爲可選屬性。

interface User {
  name: string;
  age: number;
  sex: number;
};

// User 類型的屬性是必選的,PartialUser 類型的屬性是可選的
type PartialUser = Partial<User>
/** * type PartialUser = { * name?: string; * age?: number; * sex?: number; * } */
複製代碼

Required<T>

type Required<T> = {
    [P in keyof T]-?: T[P];
};
複製代碼

Partial 相反,Required 用於把傳入類型 T 中的各項屬性轉化爲必選屬性。

interface User {
  name?: string;
  age?: number;
  sex?: number;
};

// User 類型的屬性是可選的,RequiredUser 類型的屬性是必選的
type RequiredUser = Required<User>
/** * type RequiredUser = { * name: string; * age: number; * sex: number; * } */
複製代碼

Readonly<T>

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
複製代碼

Readonly 用於將傳入類型 T 中的全部屬性轉化爲只讀屬性。

interface User {
  name: string;
  age: number;
  sex: number;
};

// User 類型的屬性是可寫的,ReadonlyUser 類型的屬性是隻讀的
type ReadonlyUser = Readonly<User>
/** * type ReadonlyUser = { * readonly name: string; * readonly age: number; * readonly sex: number; * } */
複製代碼

Pick<T, K>

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
複製代碼

Pick 用於根據傳入類型生成一個包含指定屬性的新類型,也就是說,咱們能夠從舊類型中挑出部分屬性來構成一個新的類型。

interface User {
  name: string;
  age: number;
  sex: number;
};

// PickUser 類型沒有 sex 屬性
type PickUser = Pick<User, 'name' | 'age'>
/** * type PickUser = { * name: string; * age: number; * } */
複製代碼

Record<T, K>

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
複製代碼

Record 利用一個字符串字面量類型生成一個對象類型,字符串字面量類型的各項將做爲對象類型的屬性鍵名。不一樣於以上幾個類型,Record 不是同態的,即它不須要輸入類型來拷貝屬性,而是直接建立新的屬性。

type RecordUser = Record<'name' | 'telephone' | 'description', string>
/** * type RecordUser = { * name: string; * telephone: string; * description: string; * } */
複製代碼

條件類型

另外一種經常使用的泛型結構是條件類型。條件類型能在生成一個新類型時對類型的組合轉換提供條件限制。一樣的,TS 標準庫也內置了一些經常使用的條件類型,包括 ExcludeExtractReturnTypeParameters等。

Extract<T, U>

type Extract<T, U> = T extends U ? T : never
複製代碼

Extract<T, U> 用於從聯合類型 T 中排除聯合類型 U 中不存在的成員,生成一個新的聯合類型,也就是說生成的聯合類型只包含那些 T 有且 U 也有的成員,做用相似於求交集。

type UserKey = Extract<'name' | 'age' | 'sex', 'name' | 'sex' | 'descrption'>
// type UserKey = "name" | "sex"
複製代碼

Exclude<T, U>

type Exclude<T, U> = T extends U ? never : T
複製代碼

Exclude<T, U> 用於從聯合類型 T 中排除聯合類型 U 中存在的成員,生成一個新的聯合類型,也就是說生成的聯合類型只包含那些 T 有而 U 沒有的成員,做用相似於求非集。

type UserKey = Exclude<'name' | 'age' | 'sex', 'sex'>
// type UserKey = "name" | "age"
複製代碼

利用 Exclude,咱們還能夠實現一個和 Pick 做用相反的 NeverPick

type NeverPick<T, U> = {
  [P in Exclude<keyof T, U>]: T[P];
};

interface User {
  name: string;
  age: number;
  sex: number;
};

type NeverPickUser = NeverPick<User, 'sex'>
/** * type NeverPickUser = { * name: string; * age: number; * } */
複製代碼

Parameters<T>

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
複製代碼

Parameters 當傳入的指定類型爲函數類型時,返回函數入參的類型。這裏使用了 infer 操做符來表示待推斷類型。

interface User {
  name: string;
  age: number;
  sex: number;
};

// 定義一個函數類型 GetUserFunc,入參 name 的類型是 string
interface GetUserFunc {
  (name: string): User 
}

// 提取到了 GetUserFunc 的參數類型
type funcParamsTypes = Parameters<GetUserFunc>
// type funcParamsTypes = [string]
複製代碼

ReturnType<T>

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
複製代碼

ReturnType 當傳入的指定類型爲函數類型時,返回函數返回值的類型。

interface User {
  name: string;
  age: number;
  sex: number;
};

// 定義一個函數類型 GetUserFunc,返回值的類型是 User
interface GetUserFunc {
  (name: string): User 
}

// 提取到了 GetUserFunc 的參數類型
type funcReturnTypes = ReturnType<GetUserFunc>
// type funcReturnTypes = User
複製代碼

參考文章

Typescript官方文檔-高級類型
解讀TypeScript中的泛型以及條件類型中的推斷


若是你以爲這篇內容對你有價值,歡迎點贊並關注咱們前端團隊的 官網 和咱們的微信公衆號 WecTeam,每週都有優質文章推送~

相關文章
相關標籤/搜索