天天看點

TypeScript 進階類型及用法

TypeScript 進階類型及用法

本文詳細介紹了 TypeScript 進階類型的使用場景,對日常 TypeScript 的使用可以提供一些幫助。

前言

本文已收錄在 Github: https://github.com/beichensky/Blog 中,走過路過點個 Star 呗。

一、進階類型

交叉類型(&)

交叉類型是将多個類型合并為一個類型。這讓我們可以把現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。

文法:T & U

其傳回類型既要符合 T 類型也要符合 U 類型

用法:假設有兩個接口:一個是 Ant 螞蟻接口,一個是 Fly 飛翔接口,現在有一隻會飛的螞蟻:​

interface Ant {
    name: string;
    weight: number;
}


interface Fly {
    flyHeight: number;
    speed: number;
}


// 少了任何一個屬性都會報錯
const flyAnt: Ant & Fly = {
    name: '螞蟻呀嘿',
    weight: 0.2,
    flyHeight: 20,
    speed: 1,
};      

聯合類型(|)

聯合類型與交叉類型很有關聯,但是使用上卻完全不同。

文法:T | U

其傳回類型為連接配接的多個類型中的任意一個

用法:假設聲明一個資料,既可以是 string 類型,也可以是 number 類型

let stringOrNumber:string | number = 0


stringOrNumber = ''      

再看下面這個例子,start 函數的參數類型既是 Bird | Fish,那麼在 start 函數中,想要直接調用的話,隻能調用 Bird 和 Fish 都具備的方法,否則編譯會報錯​

class Bird {
    fly() {
        console.log('Bird flying');
    }
    layEggs() {
        console.log('Bird layEggs');
    }
}


class Fish {
    swim() {
        console.log('Fish swimming');
    }
    layEggs() {
        console.log('Fish layEggs');
    }
}


const bird = new Bird();
const fish = new Fish();


function start(pet: Bird | Fish) {
    // 調用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法
    pet.layEggs();


    // 會報錯:Property 'fly' does not exist on type 'Bird | Fish'
    // pet.fly();


    // 會報錯:Property 'swim' does not exist on type 'Bird | Fish'
    // pet.swim();
}


start(bird);


start(fish);      

二、關鍵字

類型限制(extends)

文法:T extends K

這裡的 extends 不是類、接口的繼承,而是對于類型的判斷和限制,意思是判斷 T 能否指派給 K

可以在泛型中對傳入的類型進行限制。

const copy = (value: string | number): string | number => value


// 隻能傳入 string 或者 number
copy(10)


// 會報錯:Argument of type 'boolean' is not assignable to parameter of type 'string | number'
// copy(false)      

也可以判斷 T 是否可以指派給 U,可以的話傳回 T,否則傳回 never

type Exclude<T, U> = T extends U ? T : never;      

類型映射(in)

會周遊指定接口的 key 或者是周遊聯合類型​

interface Person {
    name: string
    age: number
    gender: number
}


// 将 T 的所有屬性轉換為隻讀類型
type ReadOnlyType<T> = {
    readonly [P in keyof T]: T[P]
}


// type ReadOnlyPerson = {
//     readonly name: Person;
//     readonly age: Person;
//     readonly gender: Person;
// }
type ReadOnlyPerson = ReadOnlyType<Person>      

類型謂詞(is)

文法:parameterName is Type

parameterName 必須是來自于目前函數簽名裡的一個參數名,判斷 parameterName 是否是 Type 類型。

具體的應用場景可以跟着下面的代碼思路進行使用:

看完聯合類型的例子後,可能會考慮:如果想要在 start 函數中,根據情況去調用 Bird 的 fly 方法和 Fish 的 swim 方法,該如何操作呢?

首先想到的可能是直接檢查成員是否存在,然後進行調用:

function start(pet: Bird | Fish) {
    // 調用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法
    pet.layEggs();


    if ((pet as Bird).fly) {
        (pet as Bird).fly();
    } else if ((pet as Fish).swim) {
        (pet as Fish).swim();
    }
}      

但是這樣做,判斷以及調用的時候都要進行類型轉換,未免有些麻煩,可能會想到寫個工具函數判斷下:​

function isBird(bird: Bird | Fish): boolean {
    return !!(bird as Bird).fly;
}


function isFish(fish: Bird | Fish): boolean {
    return !!(fish as Fish).swim;
}


function start(pet: Bird | Fish) {
    // 調用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法
    pet.layEggs();


    if (isBird(pet)) {
        (pet as Bird).fly();
    } else if (isFish(pet)) {
        (pet as Fish).swim();
    }
}      

看起來簡潔了一點,但是調用方法的時候,還是要進行類型轉換才可以,否則還是會報錯,那有什麼好的辦法,能讓我們判斷完類型之後,就可以直接調用方法,不用再進行類型轉換呢?

OK,肯定是有的,類型謂詞 is 就派上用場了

用法:​

function isBird(bird: Bird | Fish): bird is Bird {
    return !!(bird as Bird).fly
}


function start(pet: Bird | Fish) {
    // 調用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法
    pet.layEggs();


    if (isBird(pet)) {
        pet.fly();
    } else {
        pet.swim();
    }
};      

每當使用一些變量調用 isFish 時,TypeScript 會将變量縮減為那個具體的類型,隻要這個類型與變量的原始類型是相容的。

TypeScript 不僅知道在 if 分支裡 pet 是 Fish 類型;它還清楚在 else 分支裡,一定不是 Fish 類型,一定是 Bird 類型

待推斷類型(infer)

可以用 infer P 來标記一個泛型,表示這個泛型是一個待推斷的類型,并且可以直接使用

比如下面這個擷取函數參數類型的例子:​

type ParamType<T> = T extends (param: infer P) => any ? P : T;


type FunctionType = (value: number) => boolean


type Param = ParamType<FunctionType>;   // type Param = number


type OtherParam = ParamType<symbol>;   // type Param = symbol      

判斷 T 是否能指派給 (param: infer P) => any,并且将參數推斷為泛型 P,如果可以指派,則傳回參數類型 P,否則傳回傳入的類型

再來一個擷取函數傳回類型的例子:

type ReturnValueType<T> = T extends (param: any) => infer U ? U : T;


type FunctionType = (value: number) => boolean


type Return = ReturnValueType<FunctionType>;   // type Return = boolean


type OtherReturn = ReturnValueType<number>;   // type OtherReturn = number      

判斷 T 是否能指派給 (param: any) => infer U,并且将傳回值類型推斷為泛型 U,如果可以指派,則傳回傳回值類型 P,否則傳回傳入的類型

原始類型保護(typeof)​

文法:typeof v === "typename" 或 typeof v !== "typename"

用來判斷資料的類型是否是某個原始類型(number、string、boolean、symbol)并進行類型保護

"typename"必須是 "number", "string", "boolean"或 "symbol"。但是 TypeScript 并不會阻止你與其它字元串比較,語言不會把那些表達式識别為類型保護。

看下面這個例子, print 函數會根據參數類型列印不同的結果,那如何判斷參數是 string 還是 number 呢?

function print(value: number | string) {
    // 如果是 string 類型
    // console.log(value.split('').join(', '))


    // 如果是 number 類型
    // console.log(value.toFixed(2))
}      

有兩種常用的判斷方式:

根據是否包含 split 屬性判斷是 string 類型,是否包含 toFixed 方法判斷是 number 類型

弊端:不論是判斷還是調用都要進行類型轉換

使用類型謂詞 is

弊端:每次都要去寫一個工具函數,太麻煩了

用法:這就到了 typeof 一展身手的時候了

function print(value: number | string) {
    if (typeof value === 'string') {
        console.log(value.split('').join(', '))
    } else {
        console.log(value.toFixed(2))
    }
}      
使用 typeof 進行類型判斷後,TypeScript 會将變量縮減為那個具體的類型,隻要這個類型與變量的原始類型是相容的。

類型保護(instanceof)​

與 typeof 類似,不過作用方式不同,instanceof 類型保護是通過構造函數來細化類型的一種方式。

instanceof 的右側要求是一個構造函數,TypeScript 将細化為:

此構造函數的 prototype 屬性的類型,如果它的類型不為 any 的話

構造簽名所傳回的類型的聯合

還是以 類型謂詞 is 示例中的代碼做示範:

最初代碼:​

function start(pet: Bird | Fish) {
    // 調用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法
    pet.layEggs();


    if ((pet as Bird).fly) {
        (pet as Bird).fly();
    } else if ((pet as Fish).swim) {
        (pet as Fish).swim();
    }
}      

使用 instanceof 後的代碼:

function start(pet: Bird | Fish) {
    // 調用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法
    pet.layEggs();


    if (pet instanceof Bird) {
        pet.fly();
    } else {
        pet.swim();
    }
}      

可以達到相同的效果

索引類型查詢操作符(keyof)​

文法:keyof T

對于任何類型 T, keyof T 的結果為 T 上已知的 公共屬性名 的 聯合
interface Person {
    name: string;
    age: number;
}


type PersonProps = keyof Person; // 'name' | 'age'      

這裡,keyof Person 傳回的類型和 'name' | 'age' 聯合類型是一樣,完全可以互相替換

用法:keyof 隻能傳回類型上已知的 公共屬性名

class Animal {
    type: string;
    weight: number;
    private speed: number;
}


type AnimalProps = keyof Animal; // "type" | "weight"      

例如我們經常會擷取對象的某個屬性值,但是不确定是哪個屬性,這個時候可以使用 extends 配合 typeof 對屬性名進行限制,限制傳入的參數隻能是對象的屬性名​

const person = {
    name: 'Jack',
    age: 20
}


function getPersonValue<T extends keyof typeof person>(fieldName: keyof typeof person) {
    return person[fieldName]
}


const nameValue = getPersonValue('name')
const ageValue = getPersonValue('age')


// 會報錯:Argument of type '"gender"' is not assignable to parameter of type '"name" | "age"'
// getPersonValue('gender')      

索引通路操作符(T[K])

文法:T[K]

類似于 js 中使用對象索引的方式,隻不過 js 中是傳回對象屬性的值,而在 ts 中傳回的是 T 對應屬性 P 的類型

用法:

interface Person {
    name: string
    age: number
    weight: number | string
    gender: 'man' | 'women'
}


type NameType = Person['name']  // string


type WeightType = Person['weight']  // string | number


type GenderType = Person['gender']  // "man" | "women"      

三、映射類型​

隻讀類型(Readonly)

定義:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}      
用于将 T 類型的所有屬性設定為隻讀狀态。

用法:

interface Person {
    name: string
    age: number
}


const person: Readonly<Person> = {
    name: 'Lucy',
    age: 22
}


// 會報錯:Cannot assign to 'name' because it is a read-only property
person.name = 'Lily'      
readonly 隻讀, 被 readonly 标記的屬性隻能在聲明時或類的構造函數中指派,之後将不可改(即隻讀屬性)

隻讀數組(ReadonlyArray)​

定義:

interface ReadonlyArray<T> {
    /** Iterator of values in the array. */
    [Symbol.iterator](): IterableIterator<T>;


    /**
     * Returns an iterable of key, value pairs for every entry in the array
     */
    entries(): IterableIterator<[number, T]>;


    /**
     * Returns an iterable of keys in the array
     */
    keys(): IterableIterator<number>;


    /**
     * Returns an iterable of values in the array
     */
    values(): IterableIterator<T>;
}      

隻能在數組初始化時為變量指派,之後數組無法修改

使用:​

interface Person {
    name: string
}


const personList: ReadonlyArray<Person> = [{ name: 'Jack' }, { name: 'Rose' }]


// 會報錯:Property 'push' does not exist on type 'readonly Person[]'
// personList.push({ name: 'Lucy' })


// 但是内部元素如果是引用類型,元素自身是可以進行修改的
personList[0].name = 'Lily'      

可選類型(Partial)​

用于将 T 類型的所有屬性設定為可選狀态,首先通過 keyof T,取出類型 T 的所有屬性,

然後通過 in 操作符進行周遊,最後在屬性後加上 ?,将屬性變為可選屬性。

定義:

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

用法:

interface Person {
    name: string
    age: number
}


// 會報錯:Type '{}' is missing the following properties from type 'Person': name, age
// let person: Person = {}


// 使用 Partial 映射後傳回的新類型,name 和 age 都變成了可選屬性
let person: Partial<Person> = {}


person = { name: 'pengzu', age: 800 }


person = { name: 'z' }


person = { age: 18 }      

必選類型(Required)​

和 Partial 的作用相反

用于将 T 類型的所有屬性設定為必選狀态,首先通過 keyof T,取出類型 T 的所有屬性,

然後通過 in 操作符進行周遊,最後在屬性後的 ? 前加上 -,将屬性變為必選屬性。

定義:

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

使用:

interface Person {
    name?: string
    age?: number
}


// 使用 Required 映射後傳回的新類型,name 和 age 都變成了必選屬性
// 會報錯:Type '{}' is missing the following properties from type 'Required<Person>': name, age
let person: Required<Person> = {}      

提取屬性(Pick)​

定義:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}      
從 T 類型中提取部分屬性,作為新的傳回類型。

使用:比如我們在發送網絡請求時,隻需要傳遞類型中的部分屬性,就可以通過 Pick 來實作。​

interface Goods {
    type: string
    goodsName: string
    price: number
}


// 作為網絡請求參數,隻需要 goodsName 和 price 就可以
type RequestGoodsParams = Pick<Goods, 'goodsName' | 'price'>
// 傳回類型:
// type RequestGoodsParams = {
//     goodsName: string;
//     price: number;
// }
const params: RequestGoodsParams = {
    goodsName: '',
    price: 10
}      

排除屬性(Omit)

定義:type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

和 Pick 作用相反,用于從 T 類型中,排除部分屬性

用法:比如長方體有長寬高,而正方體長寬高相等,是以隻需要長就可以,那麼此時就可以用 Omit 來生成正方體的類型。​

interface Rectangular {
    length: number
    height: number
    width: number
}


type Square = Omit<Rectangular, 'height' | 'width'>
// 傳回類型:
// type Square = {
//     length: number;
// }


const temp: Square = { length: 5 }      

摘取類型(Extract)

文法:Extract<T, U>

提取 T 中可以 指派 給 U 的類型

定義:type Extract<T, U> = T extends U ? T : never;

用法:

type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"


type T02 = Extract<string | number | (() => void), Function>;  // () => void      

排除類型(Exclude)

文法:Exclude<T, U>

與 Extract 用法相反,從 T 中剔除可以指派給 U的類型

定義:type Exclude<T, U> = T extends U ? never : T

用法:

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"


type T01 = Exclude<string | number | (() => void), Function>;  // string | number      

屬性映射(Record)​

定義:

type Record<K extends string | number | symbol, T> = {
    [P in K]: T;
}      
接收兩個泛型,K 必須可以是可以指派給 string | number | symbol 的類型,通過 in 操作符對 K 進行周遊,每一個屬性的類型都必須是 T 類型

用法:比如我們想要将 Person 類型的數組轉化成對象映射,可以使用 Record 來指定映射對象的類型。​

interface Person {
    name: string
    age: number
}


const personList = [
    { name: 'Jack', age: 26 },
    { name: 'Lucy', age: 22 },
    { name: 'Rose', age: 18 },
]


const personMap: Record<string, Person> = {}


personList.map((person) => {
    personMap[person.name] = person
})      

比如在傳遞參數時,希望參數是一個對象,但是不确定具體的類型,就可以使用 Record 作為參數類型。

function doSomething(obj: Record<string, any>) {
}      

不可為空類型(NonNullable)​

定義:type NonNullable<T> = T extends null | undefined ? never : T

從 T 中剔除 null、undefined、never 類型,不會剔除 void、unknow 類型
type T01 = NonNullable<string | number | undefined>;  // string | number


type T02 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]


type T03 = NonNullable<{name?: string, age: number} | string[] | null | undefined>;  // {name?: string, age: number} | string[]      

構造函數參數類型(ConstructorParameters)

傳回 class 中構造函數參數類型組成的 元組類型

定義:

/**
 * Obtain the parameters of a constructor function type in a tuple
 */
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;      

使用:

class Person {
    name: string
    age: number
    weight: number
    gender: 'man' | 'women'


    constructor(name: string, age: number, gender: 'man' | 'women') {
        this.name = name
        this.age = age;
        this.gender = gender
    }
}


type ConstructorType = ConstructorParameters<typeof Person>  //  [name: string, age: number, gender: "man" | "women"]


const params: ConstructorType = ['Jack', 20, 'man']      

執行個體類型(InstanceType)

擷取 class 構造函數的傳回類型

定義:

/**
 * Obtain the return type of a constructor function type
 */
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;      

使用:​

class Person {
    name: string
    age: number
    weight: number
    gender: 'man' | 'women'


    constructor(name: string, age: number, gender: 'man' | 'women') {
        this.name = name
        this.age = age;
        this.gender = gender
    }
}


type Instance = InstanceType<typeof Person>  // Person


const params: Instance = {
    name: 'Jack',
    age: 20,
    weight: 120,
    gender: 'man'
}      

函數參數類型(Parameters)

擷取函數的參數類型組成的 元組

定義:

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;      

用法:

type FunctionType = (name: string, age: number) => boolean


type FunctionParamsType = Parameters<FunctionType>  // [name: string, age: number]


const params:  FunctionParamsType = ['Jack', 20]      

函數傳回值類型(ReturnType)

擷取函數的傳回值類型

定義:​

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;      

使用:

type FunctionType = (name: string, age: number) => boolean | string


type FunctionReturnType = ReturnType<FunctionType>  // boolean | string      

四、總結

進階類型

用法 描述
& 交叉類型,将多個類型合并為一個類型,交集
| 聯合類型,将多個類型組合成一個類型,可以是多個類型的任意一個,并集

關鍵字

用法 描述
T extends U 類型限制,判斷 T 是否可以指派給 U
P in T 類型映射,周遊 T 的所有類型
parameterName is Type 類型謂詞,判斷函數參數 parameterName 是否是 Type 類型
infer P 待推斷類型,使用 infer 标記類型 P,就可以使用待推斷的類型 P
typeof v === "typename" 原始類型保護,判斷資料的類型是否是某個原始類型(number、string、boolean、symbol)
instanceof v 類型保護,判斷資料的類型是否是構造函數的 prototype 屬性類型
keyof 索引類型查詢操作符,傳回類型上已知的 公共屬性名
T[K] 索引通路操作符,傳回 T 對應屬性 P 的類型

映射類型

用法 描述
Readonly 将 T 中所有屬性都變為隻讀
ReadonlyArray 傳回一個 T 類型的隻讀數組
ReadonlyMap<T, U> 傳回一個 T 和 U 類型組成的隻讀 Map
Partial 将 T 中所有的屬性都變成可選類型
Required 将 T 中所有的屬性都變成必選類型
Pick<T, K extends keyof T> 從 T 中摘取部分屬性
Omit<T, K extends keyof T> 從 T 中排除部分屬性
Exclude<T, U> 從 T 中剔除可以指派給 U 的類型
Extract<T, U> 提取 T 中可以指派給 U 的類型
Record<K, T> 傳回屬性名為 K,屬性值為 T 的類型
NonNullable 從 T 中剔除 null 和 undefined
ConstructorParameters 擷取 T 的構造函數參數類型組成的元組
InstanceType 擷取 T 的執行個體類型
Parameters 擷取函數參數類型組成的元組
ReturnType 擷取函數傳回值類型

本文完~

學習更多技能

請點選下方公衆号