TypeScript 高級類型及用法

來源 | https://github.com/beichensky/Blog/issues/12
前言
一、高級類型
交叉類型(&)
其返回類型既要符合 T 類型也要符合 U 類型
用法:假設有兩個接口:一個是 Ant 螞蟻接口,一個是 Fly 飛翔接口,現(xiàn)在有一只會飛的螞蟻:
interface Ant {name: string;weight: number;}interface Fly {flyHeight: number;speed: number;}// 少了任何一個屬性都會報錯const flyAnt: Ant & Fly = {name: '螞蟻呀嘿',weight: 0.2,flyHeight: 20,speed: 1,};
聯(lián)合類型(|)
聯(lián)合類型與交叉類型很有關聯(lián),但是使用上卻完全不同。
語法:T | U
其返回類型為連接的多個類型中的任意一個
用法:假設聲明一個數(shù)據(jù),既可以是 string 類型,也可以是 number 類型
let stringOrNumber:string | number = 0stringOrNumber = ''
再看下面這個例子,start 函數(shù)的參數(shù)類型既是 Bird | Fish,那么在 start 函數(shù)中,想要直接調(diào)用的話,只能調(diào)用 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) {// 調(diào)用 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 或者 numbercopy(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 或者是遍歷聯(lián)合類型
interface Person {name: stringage: numbergender: number}// 將 T 的所有屬性轉(zhuǎn)換為只讀類型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 必須是來自于當前函數(shù)簽名里的一個參數(shù)名,判斷 parameterName 是否是 Type 類型。
具體的應用場景可以跟著下面的代碼思路進行使用:
看完聯(lián)合類型的例子后,可能會考慮:如果想要在 start 函數(shù)中,根據(jù)情況去調(diào)用 Bird 的 fly 方法和 Fish 的 swim 方法,該如何操作呢?
首先想到的可能是直接檢查成員是否存在,然后進行調(diào)用:
function start(pet: Bird | Fish) {// 調(diào)用 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();}}
但是這樣做,判斷以及調(diào)用的時候都要進行類型轉(zhuǎn)換,未免有些麻煩,可能會想到寫個工具函數(shù)判斷下:
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) {// 調(diào)用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法pet.layEggs();if (isBird(pet)) {(pet as Bird).fly();} else if (isFish(pet)) {(pet as Fish).swim();}}
看起來簡潔了一點,但是調(diào)用方法的時候,還是要進行類型轉(zhuǎn)換才可以,否則還是會報錯,那有什么好的辦法,能讓我們判斷完類型之后,就可以直接調(diào)用方法,不用再進行類型轉(zhuǎn)換呢?
OK,肯定是有的,類型謂詞 is 就派上用場了
用法:
function isBird(bird: Bird | Fish): bird is Bird {return !!(bird as Bird).fly}function start(pet: Bird | Fish) {// 調(diào)用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法pet.layEggs();if (isBird(pet)) {pet.fly();} else {pet.swim();}};
每當使用一些變量調(diào)用 isFish 時,TypeScript 會將變量縮減為那個具體的類型,只要這個類型與變量的原始類型是兼容的。
TypeScript 不僅知道在 if 分支里 pet 是 Fish 類型;它還清楚在 else 分支里,一定不是 Fish 類型,一定是 Bird 類型
待推斷類型(infer)
可以用 infer P 來標記一個泛型,表示這個泛型是一個待推斷的類型,并且可以直接使用
比如下面這個獲取函數(shù)參數(shù)類型的例子:
type ParamType<T> = T extends (param: infer P) => any ? P : T;type FunctionType = (value: number) => booleantype Param = ParamType<FunctionType>; // type Param = numbertype OtherParam = ParamType<symbol>; // type Param = symbol
判斷 T 是否能賦值給 (param: infer P) => any,并且將參數(shù)推斷為泛型 P,如果可以賦值,則返回參數(shù)類型 P,否則返回傳入的類型
再來一個獲取函數(shù)返回類型的例子:
type ReturnValueType<T> = T extends (param: any) => infer U ? U : T;type FunctionType = (value: number) => booleantype Return = ReturnValueType<FunctionType>; // type Return = booleantype OtherReturn = ReturnValueType<number>; // type OtherReturn = number
判斷 T 是否能賦值給 (param: any) => infer U,并且將返回值類型推斷為泛型 U,如果可以賦值,則返回返回值類型 P,否則返回傳入的類型
原始類型保護(typeof)
語法:typeof v === "typename" 或 typeof v !== "typename"
用來判斷數(shù)據(jù)的類型是否是某個原始類型(number、string、boolean、symbol)并進行類型保護
"typename"必須是 "number", "string", "boolean"或 "symbol"。但是 TypeScript 并不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護。
看下面這個例子, print 函數(shù)會根據(jù)參數(shù)類型打印不同的結(jié)果,那如何判斷參數(shù)是 string 還是 number 呢?
function print(value: number | string) {// 如果是 string 類型// console.log(value.split('').join(', '))// 如果是 number 類型// console.log(value.toFixed(2))}
有兩種常用的判斷方式:
根據(jù)是否包含 split 屬性判斷是 string 類型,是否包含 toFixed 方法判斷是 number 類型
弊端:不論是判斷還是調(diào)用都要進行類型轉(zhuǎn)換
使用類型謂詞 is
弊端:每次都要去寫一個工具函數(shù),太麻煩了
用法:這就到了 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 類型保護是通過構(gòu)造函數(shù)來細化類型的一種方式。
instanceof 的右側(cè)要求是一個構(gòu)造函數(shù),TypeScript 將細化為:
此構(gòu)造函數(shù)的 prototype 屬性的類型,如果它的類型不為 any 的話
構(gòu)造簽名所返回的類型的聯(lián)合
還是以 類型謂詞 is 示例中的代碼做演示:
最初代碼:
function start(pet: Bird | Fish) {// 調(diào)用 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) {// 調(diào)用 layEggs 沒問題,因為 Bird 或者 Fish 都有 layEggs 方法pet.layEggs();if (pet instanceof Bird) {pet.fly();} else {pet.swim();}}
可以達到相同的效果
索引類型查詢操作符(keyof)
語法:keyof T
對于任何類型 T, keyof T 的結(jié)果為 T 上已知的 公共屬性名 的 聯(lián)合
interface Person {name: string;age: number;}type PersonProps = keyof Person; // 'name' | 'age'
這里,keyof Person 返回的類型和 'name' | 'age' 聯(lián)合類型是一樣,完全可以互相替換
用法:keyof 只能返回類型上已知的 公共屬性名
class Animal {type: string;weight: number;private speed: number;}type AnimalProps = keyof Animal; // "type" | "weight"
例如我們經(jīng)常會獲取對象的某個屬性值,但是不確定是哪個屬性,這個時候可以使用 extends 配合 typeof 對屬性名進行限制,限制傳入的參數(shù)只能是對象的屬性名
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: stringage: numberweight: number | stringgender: 'man' | 'women'}type NameType = Person['name'] // stringtype WeightType = Person['weight'] // string | numbertype GenderType = Person['gender'] // "man" | "women"
三、映射類型
只讀類型(Readonly<T>)
定義:
type Readonly<T> = {readonly [P in keyof T]: T[P];}
用于將 T 類型的所有屬性設置為只讀狀態(tài)。
用法:
interface Person {name: stringage: number}const person: Readonly<Person> = {name: 'Lucy',age: 22}// 會報錯:Cannot assign to 'name' because it is a read-only propertyperson.name = 'Lily'
readonly 只讀, 被 readonly 標記的屬性只能在聲明時或類的構(gòu)造函數(shù)中賦值,之后將不可改(即只讀屬性)
只讀數(shù)組(ReadonlyArray<T>)
定義:
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>;}
只能在數(shù)組初始化時為變量賦值,之后數(shù)組無法修改
使用:
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' })// 但是內(nèi)部元素如果是引用類型,元素自身是可以進行修改的personList[0].name = 'Lily'
可選類型(Partial<T>)
用于將 T 類型的所有屬性設置為可選狀態(tài),首先通過 keyof T,取出類型 T 的所有屬性,
然后通過 in 操作符進行遍歷,最后在屬性后加上 ?,將屬性變?yōu)榭蛇x屬性。
定義:
type Partial<T> = {[P in keyof T]?: T[P];}
用法:
interface Person {name: stringage: 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<T>)
和 Partial 的作用相反
用于將 T 類型的所有屬性設置為必選狀態(tài),首先通過 keyof T,取出類型 T 的所有屬性,
然后通過 in 操作符進行遍歷,最后在屬性后的 ? 前加上 -,將屬性變?yōu)楸剡x屬性。
定義:
type Required<T> = {[P in keyof T]-?: T[P];}
使用:
interface Person {name?: stringage?: number}// 使用 Required 映射后返回的新類型,name 和 age 都變成了必選屬性// 會報錯:Type '{}' is missing the following properties from type 'Required<Person>': name, agelet person: Required<Person> = {}
提取屬性(Pick<T>)
定義:
type Pick<T, K extends keyof T> = {[P in K]: T[P];}
從 T 類型中提取部分屬性,作為新的返回類型。
使用:比如我們在發(fā)送網(wǎng)絡請求時,只需要傳遞類型中的部分屬性,就可以通過 Pick 來實現(xiàn)。
interface Goods {type: stringgoodsName: stringprice: number}// 作為網(wǎng)絡請求參數(shù),只需要 goodsName 和 price 就可以type RequestGoodsParams = Pick<Goods, 'goodsName' | 'price'>// 返回類型:// type RequestGoodsParams = {// goodsName: string;// price: number;// }const params: RequestGoodsParams = {goodsName: '',price: 10}
排除屬性(Omit<T>)
定義:type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
和 Pick 作用相反,用于從 T 類型中,排除部分屬性
用法:比如長方體有長寬高,而正方體長寬高相等,所以只需要長就可以,那么此時就可以用 Omit 來生成正方體的類型。
interface Rectangular {length: numberheight: numberwidth: number}type Square = Omit<Rectangular, 'height' | 'width'>// 返回類型:// type Square = {// length: number;// }const temp: Square = { length: 5 }
摘取類型(Extract<T, U>)
語法: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<T, U>)
語法: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<K, T>)
定義:
type Record<K extends string | number | symbol, T> = {[P in K]: T;}
接收兩個泛型,K 必須可以是可以賦值給 string | number | symbol 的類型,通過 in 操作符對 K 進行遍歷,每一個屬性的類型都必須是 T 類型
用法:比如我們想要將 Person 類型的數(shù)組轉(zhuǎn)化成對象映射,可以使用 Record 來指定映射對象的類型。
interface Person {name: stringage: 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})
比如在傳遞參數(shù)時,希望參數(shù)是一個對象,但是不確定具體的類型,就可以使用 Record 作為參數(shù)類型。
function doSomething(obj: Record<string, any>) {}
不可為空類型(NonNullable<T>)
定義:type NonNullable<T> = T extends null | undefined ? never : T
從 T 中剔除 null、undefined、never 類型,不會剔除 void、unknow 類型
type T01 = NonNullable<string | number | undefined>; // string | numbertype T02 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[]type T03 = NonNullable<{name?: string, age: number} | string[] | null | undefined>; // {name?: string, age: number} | string[]
構(gòu)造函數(shù)參數(shù)類型(ConstructorParameters<typeof T>)
返回 class 中構(gòu)造函數(shù)參數(shù)類型組成的 元組類型
定義:
/*** 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: stringage: numberweight: numbergender: 'man' | 'women'constructor(name: string, age: number, gender: 'man' | 'women') {this.name = namethis.age = age;this.gender = gender}}type ConstructorType = ConstructorParameters<typeof Person> // [name: string, age: number, gender: "man" | "women"]const params: ConstructorType = ['Jack', 20, 'man']
實例類型(InstanceType<T>)
獲取 class 構(gòu)造函數(shù)的返回類型
定義:
/*** 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: stringage: numberweight: numbergender: 'man' | 'women'constructor(name: string, age: number, gender: 'man' | 'women') {this.name = namethis.age = age;this.gender = gender}}type Instance = InstanceType<typeof Person> // Personconst params: Instance = {name: 'Jack',age: 20,weight: 120,gender: 'man'}
函數(shù)參數(shù)類型(Parameters<T>)
獲取函數(shù)的參數(shù)類型組成的 元組
定義:
/*** 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) => booleantype FunctionParamsType = Parameters<FunctionType> // [name: string, age: number]const params: FunctionParamsType = ['Jack', 20]
函數(shù)返回值類型(ReturnType<T>)
獲取函數(shù)的返回值類型
定義:
/*** 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 | stringtype FunctionReturnType = ReturnType<FunctionType> // boolean | string
四、總結(jié)
高級類型
| 用法 | 描述 |
|---|---|
| & | 交叉類型,將多個類型合并為一個類型,交集 |
| | | 聯(lián)合類型,將多個類型組合成一個類型,可以是多個類型的任意一個,并集 |
| 用法 | 描述 |
|---|---|
| T extends U | 類型約束,判斷 T 是否可以賦值給 U |
| P in T | 類型映射,遍歷 T 的所有類型 |
| parameterName is Type | 類型謂詞,判斷函數(shù)參數(shù) parameterName 是否是 Type 類型 |
| infer P | 待推斷類型,使用 infer 標記類型 P,就可以使用待推斷的類型 P |
| typeof v === "typename" | 原始類型保護,判斷數(shù)據(jù)的類型是否是某個原始類型(number、string、boolean、symbol) |
| instanceof v | 類型保護,判斷數(shù)據(jù)的類型是否是構(gòu)造函數(shù)的 prototype 屬性類型 |
| keyof | 索引類型查詢操作符,返回類型上已知的 公共屬性名 |
| T[K] | 索引訪問操作符,返回 T 對應屬性 P 的類型 |
| 用法 | 描述 |
|---|---|
| Readonly | 將 T 中所有屬性都變?yōu)橹蛔x |
| ReadonlyArray | 返回一個 T 類型的只讀數(shù)組 |
| 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 的構(gòu)造函數(shù)參數(shù)類型組成的元組 |
| InstanceType | 獲取 T 的實例類型 |
| Parameters | 獲取函數(shù)參數(shù)類型組成的元組 |
| ReturnType | 獲取函數(shù)返回值類型 |
本文完~
學習更多技能
請點擊下方公眾號
![]()

