本文詳細介紹了 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 | 擷取函數傳回值類型 |
本文完~
學習更多技能
請點選下方公衆号