什么是鴨子??類型?
大家好,我是 ConardLi,今天我們一起來看一個 TypeScript 中一個有趣的知識點 - 鴨子類型(Duck Typing)。

什么是鴨子類型
鴨子類型是很多面向?qū)ο螅?code style="padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">OOP)語言中的常見做法。它的名字來源于所謂的“鴨子測試”:

當(dāng)看到一只鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那么這只鳥就可以被稱為鴨子。
我們不用關(guān)心鴨子的定義是什么,只要符合我們通常意義上的認知,那么他就是這個物體。在 TypeScript 中,只要對象符合定義的類型約束,那么我們就可以視為他是。
鴨子類型 通常用于需要處理一系列不同數(shù)據(jù)的代碼中,我們可能不知道調(diào)用者要傳遞哪些參數(shù)。在一些 switch 語句或復(fù)雜的 if/else 判斷中,通常是 鴨子類型 可能派上用場的地方。
為什么需要鴨子類型
在一些動態(tài)語言中,鴨子類型的常見用法就是假設(shè)給定值符合我們預(yù)期的,你可以先嘗試執(zhí)行一個操作,然后我們再去處理不符合預(yù)期的情況下的異常。比如在下面這段 Python 代碼中:
from?typing?import?Any
def?is_duck(value:?Any)?->?bool:
??try:
????value.quack()
????return?True
??except?(Attribute,?ValueError):
????return?False
這段代碼寫的很蠢,不過表達的意思挺明確的,你通過調(diào)用傳入?yún)?shù)的 .quack() 方法檢查它是否可以嘎嘎叫,如果它嘎嘎叫了,就返回 true ,如果它沒有這個方法,異常就會被捕獲,則返回 false。
在 Python 中,try-except 是一種常見的寫法,它也被很多庫(比如hasattr)廣泛使用。
相比之下,在 JavaScript 中,try-catch 則存在很多限制 — 你既不能根據(jù)拋出異常的原型定義不同的 catch 塊,也不能確定拋出的到底是不是一個異常實例。
所以,我們在處理異常的時必須更加謹慎,所以在 JavaScript 和 TypeScript 中我們要做這樣的判斷可能有點逆向思維。通常的做法可能是這樣的:
function?isDuck(value)?{
????return?!!(
????????value?&&
????????typeof?value?===?'object'?&&
????????typeof?Reflect.get(value,?'quack')?===?'function'
????);
}
在上面的函數(shù)中,我們做了下面幾個判斷:
檢查參數(shù) value是不是為空檢查參數(shù) value是否為object類型通過 Reflect.get方法更安全安全地判斷quack是不是一個函數(shù)
你可能對這種代碼再熟悉不過了,畢竟在 JavaScript 代碼里這種布爾判斷遍地都是。
如果用 TypeScript 的話寫法可能就不一樣了,參數(shù) value 可能是只鴨子,但 IDE 和 JavaScript 解析器都不知道鴨子是啥。在 TypeScript 中,我們可以把鴨子定義成一個類型:
interface?Duck?{
????quack():?string;
}
interface?Cat?{
????miao():?string;
}
function?isDuck(value:?Cat?|?Duck)?{
????return?!!(
????????value?&&
????????typeof?value?===?'object'?&&
????????typeof?value.quack?===?'function'
????);
}
這里我們在參數(shù) value 的類型中告訴 TypeScript 解析器,它可能是只鴨子也可能是只貓,你需要再函數(shù)體的邏輯中再做進一步判斷。
但是,解析器可能沒我們想象中的那么聰明,這里會報錯,因為他還是不能確定 value 到底是只鴨子還是只貓,所以無法確定 quack 函數(shù)是不是存在。
下面的寫法就可以幫我們解決這個問題:
interface?Duck?{
????quack():?string;
}
function?isDuck(value:?unknown):?value?is?Duck?{
????return?!!(
????????value?&&
????????typeof?value?===?'object'?&&
????????typeof?value.quack?===?'function'
????);
}
注意,isDuck 的返回值類型中使用了 is 關(guān)鍵字,這在 TypeScript 中被叫做類型謂詞(type predicates),類型謂詞是一個返回布爾值的函數(shù),可以用來做類型保護;
類型保護是可執(zhí)行運行時檢查的一種表達式,用于確保該類型在一定的范圍內(nèi)。換句話說,類型保護可以保證一個字符串是一個字符串,盡管它的值也可以是一個數(shù)字。
實際上它就是告訴 TypeScript 編譯器給定的值是就是我們給定的那個類型。
簡單的說,就是告訴編譯器這個可能是鴨子的東西就是一只鴨子。
用法示例 recursiveResolve
鴨子類型的一個方便用法是當(dāng)你的代碼可能接受 Promise 或者 非Promise 時來幫我們進行更優(yōu)雅的判斷。
假設(shè)我們創(chuàng)建了一個自定義方法來遞歸遍歷對象,解析可能嵌套在里面的任何 Promise,下面就是一個很好的用法:
function?isRecord<T?=?any>(value:?unknown):?value?is?Record<string?|?symbol,?T>?{
????return?!!(value?&&?typeof?value?===?'object');
}
function?isPromise<T?=?unknown>(value:?unknown):?value?is?Promise<T>?{
????return?isRecord(value)?&&?Reflect.has(value,?'then');
}
async?function?recursiveResolve<T>(
????parsedObject:?Record<string,?any>,
):?Promise<T>?{
????const?output?=?{};
????for?(const?[key,?value]?of?Object.entries(parsedObject))?{
????????const?resolved:?unknown?=?isPromise(value)???await?value?:?value;
????????if?(isRecord(resolved))?{
????????????Reflect.set(output,?key,?await?recursiveResolve(resolved));
????????}?else?{
????????????Reflect.set(output,?key,?resolved);
????????}
????}
????return?output?as?T;
}
我們在上面定義了兩個類型謂詞 - isPromiseand、isRecord,它們都接受一個可選的泛型參數(shù),這樣它們就能被復(fù)用。然后我們就可以在 recursiveResolve 函數(shù)中使用它們了,并且開銷是很小的,在整個函數(shù)中都能正確推斷輸入。
小技巧 - 通用類型保護
上面的判斷可能在我們的代碼中是個很常見的用法,如果我們需要判斷的類型有很多,為每個類型都實現(xiàn)一個這樣的類型保護函數(shù)還挺麻煩的,所以我們可以稍微做個變形來封裝一個更通用的類型保護函數(shù):
export?const?isOfType?=?(
??varToBeChecked:?any,
??propertyToCheckFor:?keyof?T
):?varToBeChecked?is?T?=>
??(varToBeChecked?as?T)[propertyToCheckFor]?!==?undefined;
你可以向這樣用:
interface?code秘密花園?{
????好文章:?string;
}
if?(isOfType(公眾號,?'好文章'))?{
??console.log('這里有好文章,這里是code秘密花園!');
}?else?{
??console.log("這里不是code秘密花園!");
}
參考
https://javascript.plainenglish.io/what-is-duck-typing-in-typescript-c537d2ff9b61 https://rangle.io/blog/how-to-use-typescript-type-guards/
往期推薦

解密初、中、高級程序員的進化之路(前端)

程序員一定會有35歲危機嗎?

近 20k Star的項目說不做就不做了,但總結(jié)的內(nèi)容值得借鑒

但凡早知道這28個網(wǎng)站,都不至于學(xué)得那么不扎實
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「huab119」拉你進技術(shù)群,長期交流學(xué)習(xí)...
關(guān)注公眾號「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。

如果覺得這篇文章還不錯,來個【轉(zhuǎn)發(fā)、收藏、在看】三連吧,讓更多的人也看到~

