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