今天給大家分享的主題是一起來做類型體操。
主要分為 4 個(gè)部分進(jìn)行介紹:
- 類型體操的背景,通過背景了解為什么要在項(xiàng)目中加入類型體操;
- 了解類型體操的主要類型、運(yùn)算邏輯、和類型套路;
-
類型體操實(shí)踐,解析 TypeScript 內(nèi)置高級(jí)類型,手寫
ParseQueryString
復(fù)雜類型; - 小結(jié),綜上分享,沉淀結(jié)論。
一、背景—
在背景章節(jié)介紹的是什么是類型,什么是類型安全,怎么實(shí)現(xiàn)類型安全,什么是類型體操?
以了解類型體操的意義。
1. 什么是類型?
了解什么是類型之前,先來介紹兩個(gè)概念:
- 不同類型變量占據(jù)的內(nèi)存大小不同
boolean 類型的變量會(huì)分配 4 個(gè)字節(jié)的內(nèi)存,而 number 類型的變量則會(huì)分配 8 個(gè)字節(jié)的內(nèi)存,給變量聲明了不同的類型就代表了會(huì)占據(jù)不同的內(nèi)存空間。
- 不同類型變量可做的操作不同
number 類型可以做加減乘除等運(yùn)算,boolean 就不可以,復(fù)合類型中不同類型的對(duì)象可用的方法不同,比如 Date 和 RegExp,變量的類型不同代表可以對(duì)該變量做的操作就不同。
綜上,可以得到一個(gè)簡(jiǎn)單的結(jié)論就是,類型就是編程語(yǔ)言提供對(duì)不同內(nèi)容的抽象定義。
2. 什么是類型安全?
了解了類型的概念后,那么,什么是類型安全呢?
一個(gè)簡(jiǎn)單的定義就是,類型安全就是只做該類型允許的操作。比如對(duì)于 boolean 類型,不允許加減乘除運(yùn)算,只允許賦值 true、false。
當(dāng)我們能做到類型安全時(shí),可以大量的減少代碼中潛在的問題,大量提高代碼質(zhì)量。
3. 怎么實(shí)現(xiàn)類型安全?
那么,怎么做到類型安全?
這里介紹兩種類型檢查機(jī)制,分別是動(dòng)態(tài)類型檢查和靜態(tài)類型檢查。
3.1 動(dòng)態(tài)類型檢查
Javascript 就是典型的動(dòng)態(tài)類型檢查,它在編譯時(shí),沒有類型信息,到運(yùn)行時(shí)才檢查,導(dǎo)致很多隱藏 bug。
3.2 靜態(tài)類型檢查
TypeScript 作為 Javascript 的超集,采用的是靜態(tài)類型檢查,在編譯時(shí)就有類型信息,檢查類型問題,減少運(yùn)行時(shí)的潛在問題。
4. 什么是類型體操
上面介紹了類型的一些定義,都是大家熟悉的一些關(guān)于類型的背景介紹,這一章節(jié)回歸到本次分享的主題概念,類型體操。
了解類型體操前,先介紹 3 種類型系統(tǒng)。
4.1 簡(jiǎn)單類型系統(tǒng)
簡(jiǎn)單類型系統(tǒng),它只基于聲明的類型做檢查,比如一個(gè)加法函數(shù),可以加整數(shù)也可以加小數(shù),但在簡(jiǎn)單類型系統(tǒng)中,需要聲明 2 個(gè)函數(shù)來做這件事情。
intadd(inta,intb){
returna+b
}
doubleadd(doublea,doubleb){
returna+b
}
4.2 泛型類型系統(tǒng)
泛型類型系統(tǒng),它支持類型參數(shù),通過給參數(shù)傳參,可以動(dòng)態(tài)定義類型,讓類型更加靈活。
Tadd(Ta,Tb){
returna+b
}
add(1,2)
add(1.1,2.2)
但是在一些需要類型參數(shù)邏輯運(yùn)算的場(chǎng)景就不適用了,比如一個(gè)返回對(duì)象某個(gè)屬性值的函數(shù)類型。
functiongetPropValue<T>(obj:T,key){
returnobj[key]
}
4.3 類型編程系統(tǒng)
類型編程系統(tǒng),它不僅支持類型參數(shù),還能給類型參數(shù)做各種邏輯運(yùn)算,比如上面提到的返回對(duì)象某個(gè)屬性值的函數(shù)類型,可以通過 keyof、T[K] 來邏輯運(yùn)算得到函數(shù)類型。
functiongetPropValue<
??Textendsobject,
KeyextendskeyofT
>(obj:T,key:Key):T[Key]{
returnobj[key]
}
總結(jié)上述,類型體操就是類型編程,對(duì)類型參數(shù)做各種邏輯運(yùn)算,以產(chǎn)生新的類型。
之所以稱之為體操,是因?yàn)樗膹?fù)雜度,右側(cè)是一個(gè)解析參數(shù)的函數(shù)類型,里面用到了很多復(fù)雜的邏輯運(yùn)算,等先介紹了類型編程的運(yùn)算方法后,再來解析這個(gè)類型的實(shí)現(xiàn)。
二、了解類型體操—
熟悉完類型體操的概念后,再來繼續(xù)了解類型體操有哪些類型,支持哪些運(yùn)算邏輯,有哪些運(yùn)算套路。
1. 有哪些類型
類型體操的主要類型列舉在圖中。TypeScript 復(fù)用了 JS 的基礎(chǔ)類型和復(fù)合類型,并新增元組(Tuple)、接口(Interface)、枚舉(Enum)等類型,這些類型在日常開發(fā)過程中類型聲明應(yīng)該都很常用,不做贅述。
//元組(Tuple)就是元素個(gè)數(shù)和類型固定的數(shù)組類型
typeTuple=[number,string];
//接口(Interface)可以描述函數(shù)、對(duì)象、構(gòu)造器的結(jié)構(gòu):
interfaceIPerson{
name:string;
age:number;
}
classPersonimplementsIPerson{
name:string;
age:number;
}
constobj:IPerson={
name:'aa',
age:18
}
//枚舉(Enum)是一系列值的復(fù)合:
enumTranspiler{
Babel='babel',
Postcss='postcss',
Terser='terser',
Prettier='prettier',
TypeScriptCompiler='tsc'
}
consttranspiler=Transpiler.TypeScriptCompiler;
2. 運(yùn)算邏輯
重點(diǎn)介紹的是類型編程支持的運(yùn)算邏輯。
TypeScript 支持條件、推導(dǎo)、聯(lián)合、交叉、對(duì)聯(lián)合類型做映射等 9 種運(yùn)算邏輯。
- 條件:T extends U ? X : Y
條件判斷和 js 邏輯相同,都是如果滿足條件就返回 a 否則返回 b。
//條件:extends?:
//如果T是2的子類型,那么類型是true,否則類型是false。
typeisTwo=Textends2?true:false;
//false
typeres=isTwo<1>;
- 約束:extends
通過約束語(yǔ)法 extends 限制類型。
//通過TextendsLength約束了T的類型,必須是包含length屬性,且length的類型必須是number。
interfaceLength{
length:number
}
functionfn1<TextendsLength>(arg:T):number{
returnarg.length
}
- 推導(dǎo):infer
推導(dǎo)則是類似 js 的正則匹配,都滿足公式條件時(shí),可以提取公式中的變量,直接返回或者再次加工都可以。
//推導(dǎo):infer
//提取元組類型的第一個(gè)元素:
//extends約束類型參數(shù)只能是數(shù)組類型,因?yàn)椴恢罃?shù)組元素的具體類型,所以用unknown。
//extends判斷類型參數(shù)T是不是[inferF,...inferR]的子類型,如果是就返回F變量,如果不是就不返回
typeFirst=Textends[inferF,...inferR]?F:never;
//1
typeres2=First<[1,2,3]>;
- 聯(lián)合:|
聯(lián)合代表可以是幾個(gè)類型之一。
typeUnion=1|2|3
- 交叉:&
交叉代表對(duì)類型做合并。
typeObjType={a:number}&{c:boolean}
- 索引查詢:keyof T
keyof 用于獲取某種類型的所有鍵,其返回值是聯(lián)合類型。
//consta:'name'|'age'='name'
consta:keyof{
name:string,
age:number
}='name'
- 索引訪問:T[K]
T[K] 用于訪問索引,得到索引對(duì)應(yīng)的值的聯(lián)合類型。
interfaceI3{
name:string,
age:number
}
typeT6=I3[keyofI3]//string|number
- 索引遍歷: in
in 用于遍歷聯(lián)合類型。
constobj={
name:'tj',
age:11
}
typeT5={
[Pinkeyoftypeofobj]:any
}
/*
{
name:any,
age:any
}
*/
- 索引重映射: as
as 用于修改映射類型的 key。
//通過索引查詢keyof,索引訪問t[k],索引遍歷in,索引重映射as,返回全新的key、value構(gòu)成的新的映射類型
typeMapType={
[
KeyinkeyofT
as`${Key&string}${Key&string}${Key&string}`
]:[T[Key],T[Key],T[Key]]
}
//{
//aaa:[1,1,1];
//bbb:[2,2,2];
//}
typeres3=MapType<{?a:1,b:2}>
3. 運(yùn)算套路
根據(jù)上面介紹的 9 種運(yùn)算邏輯,我總結(jié)了 4 個(gè)類型套路。
- 模式匹配做提?。?/li>
- 重新構(gòu)造做變換;
- 遞歸復(fù)用做循環(huán);
- 數(shù)組長(zhǎng)度做計(jì)數(shù)。
3.1 模式匹配做提取
第一個(gè)類型套路是模式匹配做提取。
模式匹配做提取的意思是通過類型 extends 一個(gè)模式類型,把需要提取的部分放到通過 infer 聲明的局部變量里。
舉個(gè)例子,用模式匹配提取函數(shù)參數(shù)類型。
typeGetParametersFunction>=
Funcextends(...args:inferArgs)=>unknown?Args:never;
typeParametersResult=GetParameters<(name:string,age:number)=>string>
首先用 extends 限制類型參數(shù)必須是 Function 類型。
然后用 extends 為 參數(shù)類型匹配公式,當(dāng)滿足公式時(shí),提取公式中的變量 Args。
實(shí)現(xiàn)函數(shù)參數(shù)類型的提取。
3.2 重新構(gòu)造做變換
第二個(gè)類型套路是重新構(gòu)造做變換。
重新構(gòu)造做變換的意思是想要變化就需要重新構(gòu)造新的類型,并且可以在構(gòu)造新類型的過程中對(duì)原類型做一些過濾和變換。
比如實(shí)現(xiàn)一個(gè)字符串類型的重新構(gòu)造。
typeCapitalizeStr=
Strextends`${inferFirst}${inferRest}`
?`${Uppercase} ${Rest}`:Str;
typeCapitalizeResult=CapitalizeStr<'tang'>
首先限制參數(shù)類型必須是字符串類型。
然后用 extends 為參數(shù)類型匹配公式,提取公式中的變量 First Rest,并通過 Uppercase 封裝。
實(shí)現(xiàn)了首字母大寫的字符串字面量類型。
3.3 遞歸復(fù)用做循環(huán)
第三個(gè)類型套路是遞歸復(fù)用做循環(huán)。
TypeScript 本身不支持循環(huán),但是可以通過遞歸完成不確定數(shù)量的類型編程,達(dá)到循環(huán)的效果。
比如通過遞歸實(shí)現(xiàn)數(shù)組類型反轉(zhuǎn)。
typeReverseArr=
Arrextends[inferFirst,...inferRest]
?[...ReverseArr,First]
:Arr;
typeReverseArrResult=ReverseArr<[1,2,3,4,5]>
首先限制參數(shù)必須是數(shù)組類型。
然后用 extends 匹配公式,如果滿足條件,則調(diào)用自身,否則直接返回。
實(shí)現(xiàn)了一個(gè)數(shù)組反轉(zhuǎn)類型。
3.4 數(shù)組長(zhǎng)度做計(jì)數(shù)
第四個(gè)類型套路是數(shù)組長(zhǎng)度做計(jì)數(shù)。
類型編程本身是不支持做加減乘除運(yùn)算的,但是可以通過遞歸構(gòu)造指定長(zhǎng)度的數(shù)組,然后取數(shù)組長(zhǎng)度的方式來完成數(shù)值的加減乘除。
比如通過數(shù)組長(zhǎng)度實(shí)現(xiàn)類型編程的加法運(yùn)算。
typeBuildArray<
????Length?extends?number,
????Ele?=?unknown,
????Arr?extends?unknown[]?=?[]
????>=Arr['length']extendsLength
?Arr
:BuildArray;
typeAdd=
[...BuildArray,...BuildArray]['length'];
typeAddResult=Add<32,25>
首先通過遞歸創(chuàng)建一個(gè)可以生成任意長(zhǎng)度的數(shù)組類型
然后創(chuàng)建一個(gè)加法類型,通過數(shù)組的長(zhǎng)度來實(shí)現(xiàn)加法運(yùn)算。
三、類型體操實(shí)踐—
分享的第三部分是類型體操實(shí)踐。
前面分享了類型體操的概念及常用的運(yùn)算邏輯。
下面我們就用這些運(yùn)算邏輯來解析 TypeScript 內(nèi)置的高級(jí)類型。
1. 解析 TypeScript 內(nèi)置高級(jí)類型
- partial 把索引變?yōu)榭蛇x
通過 in 操作符遍歷索引,為所有索引添加 ?前綴實(shí)現(xiàn)把索引變?yōu)榭蛇x的新的映射類型。
typeTPartial={
[PinkeyofT]?:T[P];
};
typePartialRes=TPartial<{?name:'aa',age:18}>
- Required 把索引變?yōu)楸剡x
通過 in 操作符遍歷索引,為所有索引刪除 ?前綴實(shí)現(xiàn)把索引變?yōu)楸剡x的新的映射類型。
typeTRequired={
[PinkeyofT]-?:T[P]
}
typeRequiredRes=TRequired<{?name?:?'aa',age?:18}>
- Readonly 把索引變?yōu)橹蛔x
通過 in 操作符遍歷索引,為所有索引添加 readonly 前綴實(shí)現(xiàn)把索引變?yōu)橹蛔x的新的映射類型。
typeTReadonly={
readonly[PinkeyofT]:T[P]
}
typeReadonlyRes=TReadonly<{?name?:?'aa',age?:18}>
- Pick 保留過濾索引
首先限制第二個(gè)參數(shù)必須是對(duì)象的 key 值,然后通過 in 操作符遍歷第二個(gè)參數(shù),生成新的映射類型實(shí)現(xiàn)。
typeTPick={
[PinK]:T[P]
}
typePickRes=TPick<{?name?:?'aa',age?:18},'name'>
- Record 創(chuàng)建映射類型
通過 in 操作符遍歷聯(lián)合類型 K,創(chuàng)建新的映射類型。
typeTRecord={
[PinK]:T
}
typeRecordRes=TRecord<'aa'|'bb',string>
- Exclude 刪除聯(lián)合類型的一部分
通過 extends 操作符,判斷參數(shù) 1 能否賦值給參數(shù) 2,如果可以則返回 never,以此刪除聯(lián)合類型的一部分。
typeTExclude=TextendsU?never:T
typeExcludeRes=TExclude<'aa'|'bb','aa'>
- Extract 保留聯(lián)合類型的一部分
和 Exclude 邏輯相反,判斷參數(shù) 1 能否賦值給參數(shù) 2,如果不可以則返回 never,以此保留聯(lián)合類型的一部分。
typeTExtract=TextendsU?T:never
typeExtractRes=TExtract<'aa'|'bb','aa'>
- Omit 刪除過濾索引
通過高級(jí)類型 Pick、Exclude 組合,刪除過濾索引。
typeTOmit=Pick>
typeOmitRes=TOmit<{?name:'aa',age:18},'name'>
- Awaited 用于獲取 Promise 的 valueType
通過遞歸來獲取未知層級(jí)的 Promise 的 value 類型。
typeTAwaited=
Textendsnull|undefined
?T
:Textendsobject&{then(onfulfilled:inferF):any}
?Fextends((value:inferV,...args:any)=>any)
?Awaited
:never
:T;
typeAwaitedRes=TAwaited<Promise<Promise<Promise>>>
還有非常多高級(jí)類型,實(shí)現(xiàn)思路和上面介紹的類型套路大多一致,這里不一一贅述。
2. 解析 ParseQueryString 復(fù)雜類型
重點(diǎn)解析的是在背景章節(jié)介紹類型體操?gòu)?fù)雜度,舉例說明的解析字符串參數(shù)的函數(shù)類型。
如圖示 demo 所示,這個(gè)函數(shù)是用于將指定字符串格式解析為對(duì)象格式。
functionparseQueryString1(queryStr){
if(!queryStr||!queryStr.length){
return{}
}
constqueryObj={}
constitems=queryStr.split('&')
items.forEach((item)=>{
const[key,value]=item.split('=')
if(queryObj[key]){
if(Array.isArray(queryObj[key])){
queryObj[key].push(value)
}else{
queryObj[key]=[queryObj[key],value]
}
}else{
queryObj[key]=value
}
})
returnqueryObj
}
比如獲取字符串 a=1&b=2 中 a 的值。
常用的類型聲明方式如下圖所示:
functionparseQueryString1(queryStr:string):Record<string,any>{
if(!queryStr||!queryStr.length){
return{}
}
constqueryObj={}
constitems=queryStr.split('&')
items.forEach((item)=>{
const[key,value]=item.split('=')
if(queryObj[key]){
if(Array.isArray(queryObj[key])){
queryObj[key].push(value)
}else{
queryObj[key]=[queryObj[key],value]
}
}else{
queryObj[key]=value
}
})
returnqueryObj
}
參數(shù)類型為 string
,返回類型為 Record
,這時(shí)看到,res1.a
類型為 any
,那么有沒有辦法,準(zhǔn)確的知道 a
的類型是字面量類型 1
呢?
下面就通過類型體操的方式,來重寫解析字符串參數(shù)的函數(shù)類型。
首先限制參數(shù)類型是 string
類型,然后為參數(shù)匹配公式 a&b
,如果滿足公式,將 a
解析為 key value
的映射類型,將 b
遞歸 ParseQueryString
類型,繼續(xù)解析,直到不再滿足 a&b
公式。
最后,就可以得到一個(gè)精準(zhǔn)的函數(shù)返回類型,res.a = 1
。
typeParseParam=
Paramextends`${inferKey}=${inferValue}`
?{
[KinKey]:Value
}:Record;
typeMergeParams<
????OneParam?extends?Record,
OtherParamextendsRecord
>={
readonly[KeyinkeyofOneParam|keyofOtherParam]:
KeyextendskeyofOneParam
?OneParam[Key]
:KeyextendskeyofOtherParam
?OtherParam[Key]
:never
}
typeParseQueryString=
Strextends`${inferParam}&${inferRest}`
?MergeParams,ParseQueryString>
:ParseParam;
functionparseQueryString<Strextendsstring>(queryStr:Str):ParseQueryString<Str>{
if(!queryStr||!queryStr.length){
return{}asany;
}
constqueryObj={}asany;
constitems=queryStr.split('&');
items.forEach(item=>{
const[key,value]=item.split('=');
if(queryObj[key]){
if(Array.isArray(queryObj[key])){
queryObj[key].push(value);
}else{
queryObj[key]=[queryObj[key],value]
}
}else{
queryObj[key]=value;
}
});
returnqueryObjasany;
}
constres=parseQueryString('a=1&b=2&c=3');
console.log(res.a)//type1
四、小結(jié)—
綜上分享,從 3 個(gè)方面介紹了類型體操。
-
第一點(diǎn)是類型體操背景,了解了什么是類型,什么是類型安全,怎么實(shí)現(xiàn)類型安全;
-
第二點(diǎn)是熟悉類型體操的主要類型、支持的邏輯運(yùn)算,并總結(jié)了 4 個(gè)類型套路;
-
第三點(diǎn)是類型體操實(shí)踐,解析了 TypeScript 內(nèi)置高級(jí)類型的實(shí)現(xiàn),并手寫了一些復(fù)雜函數(shù)類型。
從中我們了解到需要?jiǎng)討B(tài)生成類型的場(chǎng)景,必然是要用類型編程做一些運(yùn)算,即使有的場(chǎng)景下可以不用類型編程,但是使用類型編程能夠有更精準(zhǔn)的類型提示和檢查,減少代碼中潛在的問題。
審核編輯:湯梓紅
-
內(nèi)存
+關(guān)注
關(guān)注
8文章
2964瀏覽量
73803 -
編程
+關(guān)注
關(guān)注
88文章
3559瀏覽量
93533
原文標(biāo)題:類型體操的9種類型運(yùn)算、4種類型套路總結(jié)
文章出處:【微信號(hào):OSC開源社區(qū),微信公眾號(hào):OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論