TypeScript進(jìn)階之工具類型&高級(jí)類型
什么是工具類型
用 JavaScript 編寫中大型程序是離不開 lodash 工具的,而用 TypeScript 編程同樣離不開工具類型的幫助,工具類型就是類型版的 lodash 。簡(jiǎn)單的來說,就是把已有的類型經(jīng)過類型轉(zhuǎn)換構(gòu)造一個(gè)新的類型。工具類型本身也是類型,得益于泛型的幫助,使其能夠?qū)︻愋瓦M(jìn)行抽象的處理。工具類型主要目的是簡(jiǎn)化類型編程的過程,提高生產(chǎn)力。
使用工具類型的好處
先來看看一個(gè)場(chǎng)景,體會(huì)下工具類型帶來什么好處。
//?一個(gè)用戶接口
interface?User?{
??name:?string
??avatar:?string
? country:string
? friend:{
????name:?string
????sex:?string
??}
}
現(xiàn)在業(yè)務(wù)要求 User 接口里的成員都變?yōu)榭蛇x,你會(huì)怎么做?再定義一個(gè)接口,為成員都加上可選修飾符嗎?這種方法確實(shí)可行,但接口里有幾十個(gè)成員呢?此時(shí),工具類型就可以派上用場(chǎng)。
type?Partial?=?{[K?in?keyof?T]?:?T[K]}
type?PartialUser?=?Partial
//?此時(shí)PartialUser等同于
type?PartialUser?=?{
??name?:?string?|?undefined;
??avatar?:?string?|?undefined;
??country?:?string?|?undefined;
??friend?:?{
????name:?string;
????sex:?string;
??}?|?undefined;
}
通過工具類型的處理,我們得到一個(gè)新的類型。即使成員有成千上百個(gè),我們也只需要一行代碼。由于 friend 成員是對(duì)象,上面的 Partial 處理只對(duì)第一層添加可選修飾符,假如需要將對(duì)象成員內(nèi)的成員也添加可選修飾符,可以使用 Partial 遞歸來解決。
type?partial?=?{
??[K?in?keyof?T]?:?T[K]?extends?object???partial?:?T[K]
}
如果你是第一次看到以上的寫法,可能會(huì)很懵逼,不知道發(fā)生了什么操作。不慌,且往下看,或許當(dāng)你看完這篇文章再回過頭來看時(shí),會(huì)發(fā)現(xiàn)原來是這么一回事。
關(guān)鍵字
TypeScript 中的一些關(guān)鍵字對(duì)于編寫工具類型必不可缺
keyof
語法:「keyof T」 。返回聯(lián)合類型,為 T 的所有 key
interface?User{
??name:?string
??age:?number
}
type?Man?=?{?
??name:string,?
??height:?180
}
type?ManKeys?=?keyof?Man?//?"name"?|?"height"
type?UserKeys?=?keyof?User?//?"name"?|?"age"
typeof
語法:「typeof T」 。返回 T 的成員的類型
let?arr?=?['apple',?'banana',?100]
let?man?=?{
??name:?'Jeo',
??age:?20,
??height:?180
}
type?Arr?=?typeof?arr?//?(string?|?number)[]
type?Man?=?typeof?man?//?{name:?string;?age:?number;?height:?number;}
infer
相比上面兩個(gè)關(guān)鍵字, infer 的使用可能會(huì)有點(diǎn)難理解。在有條件類型的 extends 子語句中,允許出現(xiàn) infer 聲明,它會(huì)引入一個(gè)待推斷的類型變量。這個(gè)推斷的類型變量可以在有條件類型的 true 分支中被引用。
簡(jiǎn)單來說,它可以把類型處理過程的某個(gè)部分抽離出來當(dāng)做類型變量。以下例子需要結(jié)合高級(jí)類型,如果不能理解,可以選擇跳轉(zhuǎn)這部分,把高級(jí)類型看完后再回來。
下面代碼會(huì)提取函數(shù)類型的返回值類型:
type?ReturnType?=?T?extends?(...args:?any[])?=>?infer?R???R?:?any;
(...args: any[]) => infer R 和 Function 類型的作用是差不多的,這樣寫只是為了能夠在過程中拿到函數(shù)的返回值類型。infer 在這里相當(dāng)于把返回值類型聲明成一個(gè)類型變量,提供給后面的過程使用。
有條件類型可以嵌套來構(gòu)成一系列的匹配模式,按順序進(jìn)行求值:
type?Unpacked?=
??T?extends?(infer?U)[]???U?:
??T?extends?(...args:?any[])?=>?infer?U???U?:
??T?extends?Promise???U?:
??T;
type?T0?=?Unpacked<string>;??//?string
type?T1?=?Unpacked<string[]>;??//?string
type?T2?=?Unpacked<()?=>?string>;??//?string
type?T3?=?Unpacked<Promise<string>>;??//?string
type?T4?=?Unpacked<Promise<string>[]>;??//?Promise
type?T5?=?UnpackedPromise<string>[]>>;??//?string
高級(jí)類型
交叉類型
語法:「A & B」 ,交叉類型可以把多個(gè)類型合并成一個(gè)新類型,新類型將擁有所有類型的成員。
interface?Shape?{
??size:?string
??color:?string
}
interface?Brand?{
??name:?string
??price:?number
}
let?clothes:?Shape&Brand?=?{
??name:?'Uniqlo',
??color:?'blue',
??size:?'XL',
??price:?200
}
聯(lián)合類型
語法:「typeA | typeB」 ,聯(lián)合類型是包含多種類型的類型,被綁定聯(lián)合類型的成員只需滿足其中一種類型。
function?pushItem(item:string|number){
??let?array:Array<string|number>?=?['apple','banana','cherry']
??array.push(item)
}
pushItem(10)?//?ok
pushItem('durian')?//?ok
通常,刪除用戶信息需要提供 id ,創(chuàng)建用戶則不需要 id 。這種類型應(yīng)該如何定義?如果選擇為 id 字段提供添加可選修飾符的話,那就太不明智了。因?yàn)樵趧h除用戶時(shí),即使不填寫 id 屬性也不會(huì)報(bào)錯(cuò),這不是我們想要的結(jié)果。
可辨識(shí)聯(lián)合類型能幫助我們解決這個(gè)問題:
type?UserAction?=?{
??action:?'create'
}|{
??id:number
??action:?'delete'
}
let?userAction:UserAction?=?{
??id:?1,
??action:?'delete'
}
字面量類型
字?量類型主要分為 真值字?量類型,數(shù)字字?量類型,枚舉字?量類型,?整數(shù)字?量類型、字符串字?量類型。
const?a:?2333?=?2333?//?ok
const?b:?0b10?=?2?//?ok
const?c:?0x514?=?0x514?//?ok
const?d:?'apple'?=?'apple'?//?ok
const?e:?true?=?true?//?ok
const?f:?'apple'?=?'banana'?//?不能將類型“"banana"”分配給類型“"apple"”
下面以字符串字面量類型作為例子:
字符串字面量類型允許指定的字符串作為類型。如果使用 JavaScript 的模式中看下面的例子,會(huì)把 level 當(dāng)成一個(gè)值。但在 TypeScript 中,千萬不要用這種思維去看待, level 表示的就是一個(gè)字符串 coder 的類型,被綁定這個(gè)類型的變量,它的值只能是 coder 。
type?Level?=?'coder'
let?level:Level?=?'coder'?//?ok
let?level2:Level?=?'programmer'?//?不能將類型“"programmer"”分配給類型“"coder"”
字符串和聯(lián)合類型搭配,可以實(shí)現(xiàn)類似枚舉類型的字符串
type?Level?=?'coder'?|?'leader'?|?'boss'
function?getWork(level:?Level){
??if(level?===?'coder'){
????console.log('打代碼、摸魚')
??}else?if(level?===?'leader'){
????console.log('造輪子、架構(gòu)')
??}else?if(level?===?'boss'){
????console.log('喝茶、談生意')
??}
}
getWork('coder')
getWork('user')?//?類型“"user"”的參數(shù)不能賦給類型“Level”的參數(shù)
索引類型
語法:「T[K]」 ,使用索引類型,編譯器就能夠檢查使用動(dòng)態(tài)屬性名的代碼。在 JavaScript 中,對(duì)象可以用屬性名獲取值,而在 TypeScript 中,這一切被抽象化,變成通過索引獲取類型。就像 person[name] 被抽象成類型 Person[name] ,在以下例子中代表的就是 string 類型。
interface?Person?{
??name:?string;
??age:?number;
}
let?person:?Person?=?{
??name:?'Jeo',
??age:?20
}
let?name?=?person['name']?//?'Jeo'
type?str?=?Person['name']?//?string
我們可以在普通的上下文里使用 T[K] ,只要確保類型變量 K 為 T 的索引即可
function?getProperty<T,?K?extends?keyof?T>(o:?T,?name:?K):?T[K]?{
??return?o[name];?//?o[name]?is?of?type?T[K]
}
getProperty 里的 o: T 和 name: K ,意味著 o[name]: T[K]
let?name:?string?=?getProperty(person,?'name');
let?age:?number?=?getProperty(person,?'age');
let?unknown?=?getProperty(person,?'unknown');?//?類型“"unknown"”的參數(shù)不能賦給類型“"name"?|?"age"”的參數(shù)
K 不僅可以傳成員,成員的字符串聯(lián)合類型也是有效的
type?Union?=?Person[keyof?Person]?//?"string"?|?"number"
映射類型
語法:「[K in Keys]」 。TypeScript 提供了從舊類型中創(chuàng)建新類型的一種方式 。在映射類型里,新類型以相同的形式去轉(zhuǎn)換舊類型里每個(gè)屬性。根據(jù) Keys 來創(chuàng)建類型, Keys 有效值為 string | number | symbol 或 聯(lián)合類型。
type?Keys?=?'name'|10
type?User?=?{
??[K?in?Keys]:?string
}
該語法可以理解為內(nèi)部使用了循環(huán)
K: 依次綁定到每個(gè)屬性,相當(dāng)于 Keys 的項(xiàng) Keys: 包含要迭代的屬性名的集合
因此以上的例子等同于:
type?User?=?{
??name:?string;
??10:?string;
}
需要注意的是這個(gè)語法描述的是類型而非成員。若想添加額外的成員,需使用交叉類型:
//?這樣使用
type?ReadonlyWithNewMember?=?{
??readonly?[P?in?keyof?T]:?T[P];
}?&?{?newMember:?boolean?}
//?不要這樣使用
//?這會(huì)報(bào)錯(cuò)!
type?ReadonlyWithNewMember?=?{
??readonly?[P?in?keyof?T]:?T[P];
??newMember:?boolean;
}
在真正應(yīng)用中,映射類型結(jié)合索引訪問類型是一個(gè)很好的搭配。因?yàn)檗D(zhuǎn)換過程會(huì)基于一些已存在的類型,且按照一定的方式轉(zhuǎn)換字段。你可以把這過程理解為 JavaScript 中數(shù)組的 map 方法,在原本的基礎(chǔ)上擴(kuò)展元素( TypeScript 中指類型),當(dāng)然這種理解過程可能有點(diǎn)粗糙。
文章開頭的 Partial 工具類型正是使用這種搭配,為原有的類型添加可選修飾符。
條件類型
語法:「T extends U ? X : Y」 ,若 T 能夠賦值給 U ,那么類型是 X ,否則為 Y 。條件類型以條件表達(dá)式推斷類型關(guān)系,選擇其中一個(gè)分支。相對(duì)上面的類型,條件類型很好理解,類似 JavaScript 中的三目運(yùn)算符。
再來看看文章開頭遞歸的操作,你就會(huì)發(fā)現(xiàn)能看懂這段處理過程。過程:使用映射類型遍歷,判斷 T[K] 屬于 object 類型,則把 T[K] 傳入 partial 遞歸,否則返回類型 T[K] 。
type?partial?=?{
??[K?in?keyof?T]?:?T[K]?extends?object???partial?:?T[K]
}
小結(jié)
關(guān)于一些常用的高級(jí)類型相信大家都了解得差不多,下面將應(yīng)用這些類型來編寫一個(gè)工具類型。
該工具類型實(shí)現(xiàn)的功能為篩選出兩個(gè) interface 的公共成員:
interface?PersonA{
??name:?string
??age:?number
??boyfriend:?string
??car:?{
????type:?'Benz'
??}
}
interface?PersonB{
??name:?string
??age:?string
??girlfriend:?string
??car:?{
????type:?'bicycle'
??}
}
type?Filter?=?T?extends?U???T?:?never
type?Common?=?{
??[K?in?Filter]:?A[K]?extends?B[K]???A[K]?:?A[K]|B[K]
}
通過 Filter 篩選出公共的成員聯(lián)合類型 "name"|"age" 作為映射類型的集合,公共部分可能會(huì)存在類型不同的情況,因此要為成員保留兩者的類型。
type?CommonMember?=?Common
//?等同于
type?CommonMember?=?{
??name:?string;
??age:?string?|?number;
??car:?{
????type:?"Benz";
??}?|?{
????type:?"bicycle";
??};
}
內(nèi)置工具類型
為了滿足常見的類型轉(zhuǎn)換需求, TypeScript 也提供一些內(nèi)置工具類型,這些類型是全局可見的。
Partial
構(gòu)造類型 T ,并將它所有的屬性設(shè)置為可選的。它的返回類型表示輸入類型的所有子類型。
interface?Todo?{
??title:?string;
??description:?string;
}
function?updateTodo(todo:?Todo,?fieldsToUpdate:?Partial )?{
??return?{?...todo,?...fieldsToUpdate?};
}
const?todo1?=?{
??title:?'organize?desk',
??description:?'clear?clutter',
};
const?todo2?=?updateTodo(todo1,?{
??description:?'throw?out?trash',
});
Readonly
構(gòu)造類型T,并將它所有的屬性設(shè)置為readonly,也就是說構(gòu)造出的類型的屬性不能被再次賦值。
interface?Todo?{
??title:?string;
}
const?todo:?Readonly?=?{
??title:?'Delete?inactive?users',
};
todo.title?=?'Hello';?//?Error:?cannot?reassign?a?readonly?property
Record
構(gòu)造一個(gè)類型,其屬性名的類型為K,屬性值的類型為T。這個(gè)工具可用來將某個(gè)類型的屬性映射到另一個(gè)類型上。
interface?PageInfo?{
??title:?string;
}
type?Page?=?'home'?|?'about'?|?'contact';
const?x:?Record?=?{
??about:?{?title:?'about'?},
??contact:?{?title:?'contact'?},
??home:?{?title:?'home'?},
};
Pick
從類型T中挑選部分屬性K來構(gòu)造類型。
interface?Todo?{
??title:?string;
??description:?string;
??completed:?boolean;
}
type?TodoPreview?=?Pick'title'?|?'completed'>;
const?todo:?TodoPreview?=?{
??title:?'Clean?room',
??completed:?false,
};
Omit
從類型T中剔除部分屬性K來構(gòu)造類型,與Pick相反。
interface?Todo?{
??title:?string;
??description:?string;
??completed:?boolean;
}
type?TodoPreview?=?Omit'title'?|?'completed'>;
const?todo:?TodoPreview?=?{
??description:?'I?am?description'
};
Exclude
從類型T中剔除所有可以賦值給U的屬性,然后構(gòu)造一個(gè)類型。
type?T0?=?Exclude<"a"?|?"b"?|?"c",?"a">;??//?"b"?|?"c"
type?T1?=?Exclude<"a"?|?"b"?|?"c",?"a"?|?"b">;??//?"c"
type?T2?=?Exclude<string?|?number?|?(()?=>?void),?Function>;??//?string?|?number
Extract
從類型T中提取所有可以賦值給U的類型,然后構(gòu)造一個(gè)類型。
type?T0?=?Extract<"a"?|?"b"?|?"c",?"a"?|?"f">;??//?"a"
type?T1?=?Extract<string?|?number?|?(()?=>?void),?Function>;??//?()?=>?void
NonNullable
從類型T中剔除null和undefined,然后構(gòu)造一個(gè)類型。
type?T0?=?NonNullable<string?|?number?|?undefined>;??//?string?|?number
type?T1?=?NonNullable<string[]?|?null?|?undefined>;??//?string[]
ReturnType
由函數(shù)類型T的返回值類型構(gòu)造一個(gè)類型。
type?T0?=?ReturnType<()?=>?string>;??//?string
type?T1?=?ReturnType<(s:?string)?=>?void>;??//?void
type?T2?=?ReturnType<(()?=>?T )>;??//?{}
type?T3?=?ReturnType<(extends ?U,?U?extends?number[]>()?=>?T)>;??//?number[]
type?T5?=?ReturnType<any>;??//?any
type?T6?=?ReturnType<never>;??//?any
type?T7?=?ReturnType<string>;??//?Error
type?T8?=?ReturnType<Function>;??//?Error
InstanceType
由構(gòu)造函數(shù)類型T的實(shí)例類型構(gòu)造一個(gè)類型。
class?C?{
??x?=?0;
??y?=?0;
}
type?T0?=?InstanceType<typeof?C>;??//?C
type?T1?=?InstanceType<any>;??//?any
type?T2?=?InstanceType;??//?any
type?T3?=?InstanceType<string>;??//?Error
type?T4?=?InstanceType<Function>;??//?Error
let?t0:T0?=?{
??x:?10,
??y:?2
}
Required
構(gòu)造一個(gè)類型,使類型T的所有屬性為required。
interface?Props?{
??a?:?number;
??b?:?string;
};
const?obj:?Props?=?{?a:?5?};?//?OK
const?obj2:?Required?=?{?a:?5?};?//?Error:?property?'b'?missing
寫在最后
除了介紹編寫工具類型所需要具備的一些知識(shí)點(diǎn),以及 TypeScript 內(nèi)置的工具類型。更重要的是抽象思維能力,不難發(fā)現(xiàn)上面的例子大部分沒有具體的值運(yùn)算,都是使用類型在編程。想要理解這些知識(shí),必須要進(jìn)入到抽象邏輯里思考。還有高級(jí)類型的搭配和類型轉(zhuǎn)換的處理,也要通過大量的實(shí)踐才能玩好。說實(shí)話,自己學(xué)習(xí)這些知識(shí)時(shí),真正感受到 TypeScript 的深不可測(cè),也了解到自身的不足之處。突然想起在某篇文章的一句話:技術(shù)是無止盡的,接觸的越多,越能感到自己的渺小。
參考資料
Typescript Hankbook(中文版)
