2021-11-13

本文并不完全遵循原文翻譯榛做,對部分內(nèi)容自己也做了解釋補充。

Narrowing

試想我們有這樣一個函數(shù)橘原,函數(shù)名為 padLeft:

functionpadLeft(padding:number|string, input:string):string{thrownewError("Not implemented yet!");}

該函數(shù)實現(xiàn)的功能是:

如果參數(shù)?padding?是一個數(shù)字,我們就在?input?前面添加同等數(shù)量的空格,而如果?padding?是一個字符串滚粟,我們就直接添加到?input?前面。

讓我們實現(xiàn)一下這個邏輯:

functionpadLeft(padding:number|string, input:string) {returnnewArray(padding +1).join(" ") + input;// Operator '+' cannot be applied to types 'string | number' and 'number'.}

如果這樣寫的話刃泌,編輯器里?padding + 1?這個地方就會標紅凡壤,顯示一個錯誤。

這是 TypeScript 在警告我們耙替,如果把一個?number?類型 (即例子里的數(shù)字 1 )和一個?number | string?類型相加鲤遥,也許并不會達到我們想要的結果。換句話說林艘,我們應該先檢查下?padding?是否是一個?number,或者處理下當?padding?是?string?的情況混坞,那我們可以這樣做:

functionpadLeft(padding:number|string, input:string) {if(typeofpadding ==="number") {returnnewArray(padding +1).join(" ") + input;? }returnpadding + input;}

這個代碼看上去也許沒有什么有意思的地方狐援,但實際上钢坦,TypeScript 在背后做了很多東西。

TypeScript 要學著分析這些使用了靜態(tài)類型的值在運行時的具體類型啥酱。目前 TypeScript 已經(jīng)實現(xiàn)了比如?if/else?爹凹、三元運算符、循環(huán)镶殷、真值檢查等情況下的類型分析禾酱。

在我們的?if?語句中,TypeScript 會認為?typeof padding === number?是一種特殊形式的代碼绘趋,我們稱之為類型保護 (type guard)颤陶,TypeScript 會沿著執(zhí)行時可能的路徑,分析值在給定的位置上最具體的類型陷遮。

TypeScript 的類型檢查器會考慮到這些類型保護和賦值語句滓走,而這個將類型推導為更精確類型的過程,我們稱之為收窄 (narrowing)帽馋。 在編輯器中搅方,我們可以觀察到類型的改變:

從上圖中可以看到在?if?語句中,和剩余的?return?語句中绽族,padding?的類型都推導為更精確的類型姨涡。

接下來,我們就介紹?narrowing?所涉及的各種內(nèi)容吧慢。

typeof 類型保護(type guards)

JavaScript 本身就提供了?typeof?操作符涛漂,可以返回運行時一個值的基本類型信息,會返回如下這些特定的字符串:

"string"

"number"

"bigInt"

"boolean"

"symbol"

"undefined"

"object"

"function"

typeof?操作符在很多 JavaScript 庫中都有著廣泛的應用娄蔼,而 TypeScript 已經(jīng)可以做到理解并在不同的分支中將類型收窄怖喻。

在 TypeScript 中,檢查?typeof?返回的值就是一種類型保護岁诉。TypeScript 知道?typeof?不同值的結果凛澎,它也能識別 JavaScript 中一些怪異的地方泉瞻,就比如在上面的列表中,typeof?并沒有返回字符串?null,看下面這個例子:

functionprintAll(strs:string|string[] |null) {if(typeofstrs ==="object") {for(constsofstrs) {// Object is possibly 'null'.console.log(s);? ? }? }elseif(typeofstrs ==="string") {console.log(strs);? }else{// do nothing}}

在這個?printAll?函數(shù)中麸折,我們嘗試判斷?strs?是否是一個對象,原本的目的是判斷它是否是一個數(shù)組類型储笑,但是在 JavaScript 中孕暇,typeof null?也會返回?object。而這是 JavaScript 一個不幸的歷史事故只搁。

熟練的用戶自然不會感到驚訝音比,但也并不是所有人都如此熟練。不過幸運的是氢惋,TypeScript 會讓我們知道?strs?被收窄為?strings[] | null?洞翩,而不僅僅是?string[]稽犁。

真值收窄(Truthiness narrowing)

在 JavaScript 中,我們可以在條件語句中使用任何表達式骚亿,比如?&&?已亥、||、!?等来屠,舉個例子虑椎,像?if?語句就不需要條件的結果總是?boolean?類型

functiongetUsersOnlineMessage(numUsersOnline:number) {if(numUsersOnline) {return`There are${numUsersOnline}online now!`;? }return"Nobody's here. :(";}

這是因為 JavaScript 會做隱式類型轉(zhuǎn)換,像?0?俱笛、NaN捆姜、""、0n嫂粟、null?undefined?這些值都會被轉(zhuǎn)為?false娇未,其他的值則會被轉(zhuǎn)為?true。

當然你也可以使用?Boolean?函數(shù)強制轉(zhuǎn)為?boolean?值星虹,或者使用更加簡短的!!:

// both of these result in 'true'Boolean("hello");// type: boolean, value: true!!"world";// type: true,? ? value: true

這種使用方式非常流行零抬,尤其適用于防范?null和?undefiend?這種值的時候。舉個例子宽涌,我們可以在?printAll?函數(shù)中這樣使用:

functionprintAll(strs:string|string[] |null) {if(strs &&typeofstrs ==="object") {for(constsofstrs) {console.log(s);? ? }? }elseif(typeofstrs ==="string") {console.log(strs);? }}

可以看到通過這種方式平夜,成功的去除了錯誤。

https://zhuanlan.zhihu.com/p/432965911

https://zhuanlan.zhihu.com/p/432966586

https://zhuanlan.zhihu.com/p/432969442

https://zhuanlan.zhihu.com/p/432970841

https://zhuanlan.zhihu.com/p/432971528

https://zhuanlan.zhihu.com/p/432973353

但還是要注意卸亮,在基本類型上的真值檢查很容易導致錯誤忽妒,比如,如果我們這樣寫?printAll?函數(shù):

functionprintAll(strs:string|string[] |null) {// !!!!!!!!!!!!!!!!//? DON'T DO THIS!//? KEEP READING// !!!!!!!!!!!!!!!!if(strs) {if(typeofstrs ==="object") {for(constsofstrs) {console.log(s);? ? ? }? ? }elseif(typeofstrs ==="string") {console.log(strs);? ? }? }}

我們把原本函數(shù)體的內(nèi)容包裹在一個?if (strs)?真值檢查里兼贸,這里有一個問題段直,就是我們無法正確處理空字符串的情況。如果傳入的是空字符串溶诞,真值檢查判斷為?false鸯檬,就會進入錯誤的處理分支。

如果你不熟悉 JavaScript 螺垢,你應該注意這種情況喧务。

另外一個通過真值檢查收窄類型的方式是通過!操作符。

functionmultiplyAll(values:number[] |undefined,? factor:number):number[] |undefined{if(!values) {returnvalues;// (parameter) values: undefined}else{returnvalues.map((x) =>x * factor);// (parameter) values: number[]}}

等值收窄(Equality narrowing)

Typescript 也會使用?switch?語句和等值檢查比如?==?!==?==?!=?去收窄類型枉圃。比如:

在這個例子中功茴,我們判斷了?x?和?y?是否完全相等,如果完全相等孽亲,那他們的類型肯定也完全相等坎穿。而?string?類型就是?x?和?y?唯一可能的相同類型。所以在第一個分支里返劲,x?和?y?就一定是?string?類型玲昧。

判斷具體的字面量值也能讓 TypeScript 正確的判斷類型犯祠。在上一節(jié)真值收窄中,我們寫下了一個沒有正確處理空字符串情況的?printAll?函數(shù)酌呆,現(xiàn)在我們可以使用一個更具體的判斷來排除掉?null?的情況:

JavaScript 的寬松相等操作符如?==?和?!=?也可以正確的收窄。在 JavaScript 中搔耕,通過?== null?這種方式并不能準確的判斷出這個值就是?null隙袁,它也有可能是?undefined?。對?== undefined?也是一樣弃榨,不過利用這點菩收,我們可以方便的判斷一個值既不是?null?也不是?undefined:

in 操作符收窄

JavaScript 中有一個?in?操作符可以判斷一個對象是否有對應的屬性名。TypeScript 也可以通過這個收窄類型鲸睛。

舉個例子娜饵,在?"value" in x?中,"value"?是一個字符串字面量官辈,而?x?是一個聯(lián)合類型:

typeFish= {swim:() =>void};typeBird= {fly:() =>void};functionmove(animal: Fish | Bird) {if("swim"inanimal) {returnanimal.swim();// (parameter) animal: Fish}returnanimal.fly();// (parameter) animal: Bird}

通過?"swim" in animal?箱舞,我們可以準確的進行類型收窄。

而如果有可選屬性拳亿,比如一個人類既可以?swim?也可以?fly?(借助裝備)晴股,也能正確的顯示出來:

typeFish= {swim:() =>void};typeBird= {fly:() =>void};typeHuman= { swim?:() =>void; fly?:() =>void};functionmove(animal: Fish | Bird | Human) {if("swim"inanimal) {? ? animal;// (parameter) animal: Fish | Human}else{? ? animal;// (parameter) animal: Bird | Human}}

instanceof 收窄

instanceof?也是一種類型保護,TypeScript 也可以通過識別?instanceof?正確的類型收窄:

賦值語句(Assignments)

TypeScript 可以根據(jù)賦值語句的右值肺魁,正確的收窄左值电湘。

注意這些賦值語句都有有效的,即便我們已經(jīng)將?x?改為?number?類型鹅经,但我們依然可以將其更改為?string?類型寂呛,這是因為?x?最初的聲明為?string | number,賦值的時候只會根據(jù)正式的聲明進行核對瘾晃。

所以如果我們把?x?賦值給一個 boolean 類型贷痪,就會報錯:

控制流分析(Control flow analysis)

至此我們已經(jīng)講了 TypeScript 中一些基礎的收窄類型的例子,現(xiàn)在我們看看在?if?while等條件控制語句中的類型保護酗捌,舉個例子:

functionpadLeft(padding:number|string, input:string) {if(typeofpadding ==="number") {returnnewArray(padding +1).join(" ") + input;? }returnpadding + input;}

在第一個?if?語句里呢诬,因為有?return?語句,TypeScript 就能通過代碼分析胖缤,判斷出在剩余的部分?return padding + input?尚镰,如果 padding 是?number?類型,是無法達到 (unreachable) 這里的哪廓,所以在剩余的部分狗唉,就會將?number類型從?number | string?類型中刪除掉。

這種基于可達性(reachability) 的代碼分析就叫做控制流分析(control flow analysis)涡真。在遇到類型保護和賦值語句的時候分俯,TypeScript 就是使用這樣的方式收窄類型肾筐。而使用這種方式,一個變量可以被觀察到變?yōu)椴煌念愋停?/p>

類型判斷式(type predicates)

在有的文檔里缸剪,?type predicates?會被翻譯為類型謂詞吗铐。考慮到 predicate 作為動詞還有表明杏节、聲明唬渗、斷言的意思,區(qū)分于類型斷言(Type Assertion)奋渔,這里我就索性翻譯成類型判斷式镊逝。

如果引用這段解釋:

In?mathematics, a?predicate?is commonly understood to be a?Boolean-valued function_ P_: _X_→ {true, false}, called the predicate on _X_.

所謂?predicate?就是一個返回?boolean?值的函數(shù)。

那我們接著往下看嫉鲸。

如果你想直接通過代碼控制類型的改變撑蒜, 你可以自定義一個類型保護。實現(xiàn)方式是定義一個函數(shù)玄渗,這個函數(shù)返回的類型是類型判斷式座菠,示例如下:

functionisFish(pet: Fish | Bird): pet isFish{return(petasFish).swim!==undefined;}

在這個例子中,pet is Fish就是我們的類型判斷式捻爷,一個類型判斷式采用?parameterName is Type的形式辈灼,但?parameterName?必須是當前函數(shù)的參數(shù)名。

當 isFish 被傳入變量進行調(diào)用也榄,TypeScript 就可以將這個變量收窄到更具體的類型:

// Both calls to 'swim' and 'fly' are now okay.letpet =getSmallPet();if(isFish(pet)) {? pet.swim();// let pet: Fish}else{? pet.fly();// let pet: Bird}

注意這里巡莹,TypeScript 并不僅僅知道?if?語句里的?pet?是?Fish?類型,也知道在?else?分支里甜紫,pet?是?Bird?類型降宅,畢竟?pet?就兩個可能的類型。

你也可以用?isFish?在?Fish | Bird?的數(shù)組中囚霸,篩選獲取只有?Fish?類型的數(shù)組:

constzoo: (Fish|Bird)[] = [getSmallPet(),getSmallPet(),getSmallPet()];constunderWater1:Fish[] = zoo.filter(isFish);// or, equivalentlyconstunderWater2:Fish[] = zoo.filter(isFish)asFish[];// 在更復雜的例子中腰根,判斷式可能需要重復寫constunderWater3:Fish[] = zoo.filter((pet): pet isFish=> {if(pet.name==="sharkey")returnfalse;returnisFish(pet);});

可辨別聯(lián)合(Discriminated unions)

讓我們試想有這樣一個處理?Shape?(比如?Circles、Squares?)的函數(shù)拓型,Circles?會記錄它的半徑屬性额嘿,Squares?會記錄它的邊長屬性,我們使用一個?kind?字段來區(qū)分判斷處理的是?Circles?還是?Squares劣挫,這是初始的?Shape?定義:

interfaceShape {kind:"circle"|"square";? radius?:number;? sideLength?:number;}

注意這里我們使用了一個聯(lián)合類型册养,"circle" | "square"?,使用這種方式压固,而不是一個?string球拦,我們可以避免一些拼寫錯誤的情況:

functionhandleShape(shape: Shape) {// oops!if(shape.kind==="rect") {// This condition will always return 'false' since the types '"circle" | "square"' and '"rect"' have no overlap.// ...}}

現(xiàn)在我們寫一個獲取面積的?getArea?函數(shù),而圓和正方形的計算面積的方式有所不同,我們先處理一下是?Circle?的情況:

functiongetArea(shape: Shape) {returnMath.PI* shape.radius**2;// 圓的面積公式 S=πr2// Object is possibly 'undefined'.}

在?strictNullChecks?模式下坎炼,TypeScript 會報錯愧膀,畢竟?radius?的值確實可能是?undefined,那如果我們根據(jù)?kind?判斷一下呢谣光?

functiongetArea(shape: Shape) {if(shape.kind==="circle") {returnMath.PI* shape.radius**2;// Object is possibly 'undefined'.}}

你會發(fā)現(xiàn)檩淋,TypeScript 依然在報錯,即便我們判斷?kind?是?circle?的情況萄金,但由于?radius?是一個可選屬性狼钮,TypeScript 依然會認為?radius?可能是?undefined。

我們可以嘗試用一個非空斷言 (non-null assertion), 即在?shape.radius?加一個?!?來表示?radius?是一定存在的捡絮。

functiongetArea(shape: Shape) {if(shape.kind==="circle") {returnMath.PI* shape.radius! **2;? }}

但這并不是一個好方法,我們不得不用一個非空斷言來讓類型檢查器確信此時?shape.raidus?是存在的莲镣,我們在 radius 定義的時候?qū)⑵湓O為可選屬性福稳,但又在這里將其認為一定存在,前后語義也是不符合的瑞侮。所以讓我們想想如何才能更好的定義的圆。

此時?Shape的問題在于類型檢查器并沒有方法根據(jù)?kind?屬性判斷?radius?和?sideLength?屬性是否存在,而這點正是我們需要告訴類型檢查器的半火,所以我們可以這樣定義?Shape:

interfaceCircle {kind:"circle";radius:number;}interfaceSquare {kind:"square";sideLength:number;}typeShape=Circle|Square;

在這里越妈,我們把?Shape?根據(jù)?kind?屬性分成兩個不同的類型,radius?和?sideLength?在各自的類型中被定義為?required钮糖。

https://zhuanlan.zhihu.com/p/432955990

https://zhuanlan.zhihu.com/p/432957536

https://zhuanlan.zhihu.com/p/432958420

https://zhuanlan.zhihu.com/p/432962330

https://zhuanlan.zhihu.com/p/432963856

讓我們看看如果直接獲取?radius?會發(fā)生什么梅掠?

functiongetArea(shape: Shape) {returnMath.PI* shape.radius**2;Property'radius'does not exist ontype'Shape'.Property'radius'does not exist ontype'Square'.}

就像我們第一次定義?Shape?那樣,依然有錯誤店归。

當最一開始定義?radius?是?optional?的時候阎抒,我們會得到一個報錯 (strickNullChecks?模式下),因為 TypeScript 并不能判斷出這個屬性是一定存在的消痛。

而現(xiàn)在報錯且叁,是因為?Shape?是一個聯(lián)合類型,TypeScript 可以識別出?shape?也可能是一個?Square秩伞,而?Square?并沒有?radius逞带,所以會報錯。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纱新,一起剝皮案震驚了整個濱河市展氓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌怒炸,老刑警劉巖带饱,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡勺疼,警方通過查閱死者的電腦和手機教寂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來执庐,“玉大人酪耕,你說我怎么就攤上這事」焯剩” “怎么了迂烁?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長递鹉。 經(jīng)常有香客問我盟步,道長,這世上最難降的妖魔是什么躏结? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任却盘,我火速辦了婚禮,結果婚禮上媳拴,老公的妹妹穿的比我還像新娘黄橘。我一直安慰自己,他們只是感情好屈溉,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布塞关。 她就那樣靜靜地躺著,像睡著了一般子巾。 火紅的嫁衣襯著肌膚如雪帆赢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天线梗,我揣著相機與錄音匿醒,去河邊找鬼。 笑死缠导,一個胖子當著我的面吹牛廉羔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播僻造,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼憋他,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了髓削?” 一聲冷哼從身側響起竹挡,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎立膛,沒想到半個月后揪罕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梯码,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年好啰,在試婚紗的時候發(fā)現(xiàn)自己被綠了轩娶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡框往,死狀恐怖鳄抒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情椰弊,我是刑警寧澤许溅,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站秉版,受9級特大地震影響贤重,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜清焕,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一游桩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧耐朴,春花似錦、人聲如沸盹憎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陪每。三九已至影晓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間檩禾,已是汗流浹背挂签。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盼产,地道東北人饵婆。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像戏售,于是被迫代替她去往敵國和親侨核。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容