本文并不完全遵循原文翻譯榛做,對部分內(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逞带,所以會報錯。