iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 24
1
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 24

Day 24. 機動藍圖・類別推論 X 註記類別 - Class Type Inference & Annotation

https://ithelp.ithome.com.tw/upload/images/20190924/20120614KxDeeVL6Je.png

閱讀本篇文章前,仔細想想看

  1. 大致上已經了解類別的基本用法與性質了嗎?
  2. TypeScript 針對物件方面的型別推論與註記機制為何?

如果還沒理解完畢的話,可以先翻看以下的文章喔:

筆者事實上是因為寫類別這個主題過程太投入,差點忘記還要講類別的推論與註記過程。XD

所以我們趕緊就正文開始吧!

類別在 TypeScript 的推論與註記機制

類別的型別推論 Type Inference in Class

基本上,讀者看到今天的文章,應該已經對型別的推論與註記概念很了解。如果是跳到後面看到本篇文章時,建議可以先看筆者一開始列的文章列表喔,其中《前線維護》篇章主要是讓讀者能夠理解 Type Inference 與 Annotation 的定義與機制 —— 因為這可是 TypeScript 的主打 Feature 呢!

以下筆者就派出本日的範例類別 Horse

https://ithelp.ithome.com.tw/upload/images/20190923/20120614pwG8zSEoRj.png

(筆者知道這個類別範例很白癡,將就一下XD)

首先,以上的類別 Horse 有四個基本成員變數(Member Variables),分別代表:

  • name:馬的名稱,字串型態,而且可以供外部使用(public 模式)
  • color:馬的顏色,設定為列舉型態,顏色也可以被外面竄改
  • type:馬的種類,為字串型態;儘管為 public 模式但卻被標記 readonly —— 唯讀模式
  • noise:馬的叫聲,但是是 private 模式,而且有預設值(讀者會學這種 'MeeeeeeeEeeééeéeée~' 的叫聲嗎?)

以上的說明,筆者真的沒有在罵粗話 XD

再來也有幾個成員方法(Member Methods):

  • makeNoisepublic 模式,負責讓馬叫(叫啊!
  • info 也是 public 模式,負責印出馬的基本資料
  • infoText 則是 private 模式,負責將馬的基本資料用 Template String 湊合起來

理解之後,今天的目標是要理解類別的型別推論與註記機制。

因此我們先變出一隻馬,但是完整建構馬之前:筆者必須截下這張圖。(建構物件時 VSCode 提醒視窗如圖一)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614IXdfTnnWR9.png
圖一:還沒建構物件前,TypeScript 提醒我們 Horse建構子函式的參數被推論的結果

很明顯看到:

Horse(name: string, color: Color, type: string, noise?: string): Horse

這是屬於函式型別推論結果,光是這一行就已經告訴我們很多很有用的訊息呢!

  1. 函式名稱為 Horse —— 也就是說宣告類別(Class)就相當於宣告一個函式 —— 原生 JS 在模擬類別的情形時,就是用 Function 來模擬的。
  2. 函式 Horse回傳型別是 Horse,代表類別本身是一種型別化名(Type Alias);也就是說,宣告一個類別等於建造新的型別,因此也印證筆者之前在類別繼承篇章偶然提到的概念:“類別與介面跟普通的型別一樣,都是型別化名的一種”。
  3. 圖一中,仔細看視窗被底線與粗體凸顯的訊息:name: string,TypeScript 提示我們 —— 第一個參數要填入參數型態為 string,代表 name 這個參數

根據剛剛筆者講的第 3 點,假設我們已經填好第一個參數,那麼下一個畫面出現的是(如圖二):

https://ithelp.ithome.com.tw/upload/images/20190923/20120614PIQ0s1zOoY.png
圖二:第一個參數填完,自動提示你第二個參數為 color: Color

以上是筆者想要提醒讀者 —— 要好好善用 TypeScript 提供給你的工具,增進開發效率的小細節不要輕易放過。

另外,有時候注意到工具提供的訊息 —— 可以猜出一些 TypeScript 隱藏的機制,很方便呢!等等筆者繼續驗證類別的註記部分,基本上又會再把 Day 03. 提到的完整性理論再次搬出來喔。

首先先產出一隻小馬:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614CEoqAJxaJg.png

ㄧ樣來看看類別建造過後的物件被推論的結果是什麼。(推論結果如圖三)

https://ithelp.ithome.com.tw/upload/images/20190923/2012061444HjjNo1W1.png
圖三:aRandomHorsie 的推論結果是 Horse 型別

恩,筆者來下一個很根本(但很廢話)的重點

重點 1. 類別型別

宣告新的類別 —— 本身就是在創造新的型別化名;也就是說,我們可以使用類別名稱作為變數的型別註記(Type Annotation)。

根據很久之前描述的廣義物件完整性原則

  • 我們不能破壞物件的完整性 —— 新增屬性會被 TypeScript 喊卡!
  • 我們不能破壞物件的完整性 —— 對物件原本有的屬性指派錯誤型別的值會被 TypeScript 喊卡!
  • 我們不能破壞物件的完整性 —— 覆寫整個值就必須覆寫完整且正確的格式,亦即要覆蓋掉屬於 Horse 型別的變數的值,必須用 Horse 類別建構出來的物件進行完美覆蓋動作

以下的程式碼就是對以上的案例的驗證行為。(驗證結果如圖四;錯誤訊息如圖五~圖七)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614CeMz0wwY3J.png

https://ithelp.ithome.com.tw/upload/images/20190923/20120614CEAhrJzHm7.png
圖四:各種層面的出錯,TypeScript 會主動河蟹

https://ithelp.ithome.com.tw/upload/images/20190923/20120614DZB8a74Hyf.png
圖五:難道紅色就錯了嗎?

https://ithelp.ithome.com.tw/upload/images/20190923/20120614RfB9mkjSga.png
圖六:難道豢養的馬也錯了嗎?

https://ithelp.ithome.com.tw/upload/images/20190923/20120614Ob1K6waikk.png
圖七:難道馬不見了也是我的錯,開什麼玩笑?

以上就是驗證過後的類別推論行為的機制,也符合完整性原則。

重點 2. 類別的型別推論機制 Type Reference in Class

若變數被指派的值為類別 C 建構出來的物件,則 TypeScript 會自動推論該變數之型別為 C

被推論出型別為 C 的變數符合廣義物件完整性原則

  1. 該變數不能夠新增屬性
  2. 該變數在原有屬性下不能指派錯誤的型別的值
  3. 完整覆寫該變數,指派的值必須是類別 C 建構出來的物件

類別的型別註記 Type Annotation of Class

恩,用 of 作為副標的介系詞應該會比 in 適合 <-- 這是 P 話不要理會筆者)

型別註記其實沒什麼好講的,就是以下幾種註記方式:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614ynD7pIZhxV.png

這邊就留給讀者自己試試看囉。

我們還沒討論完全部的推論與註記行為!

哦哦哦哦~~接下來才是很整人的部分 —— 我們要討論的 Case 很多,不光只有討論單純類別的型別推論與註記行為,這樣交代實在是太不負責任啦~~~XD。(笑 P 笑

筆者要討論的狀況有這些:

  1. 普通類別之型別推論與註記行為(剛討論完)
  2. 繼承過後的類別之型別推論與註記行為
  3. 類別實踐介面時之型別推論與註記行為
  4. 類別實踐介面時,類別繼承之型別推論與註記行為

類別跟介面想當然會有 4 種組合。不過呢,類別也可以結合普通型別 —— 實踐 type 宣告的型別化名裡的靜態資料格式,硬要討論總共有 8 種組合(但絕對不會是八大行業)。

所以事實上,你也可以這麼做,這是之前沒提到的:

type SomeType = {
  message: string;
}

class SomeClass implements SomeType {
  /* ... 略 */
}

然而,筆者不鼓勵類別與型別 type 結合的原因還是一句話:“型別代表的意義是靜態資料格式,類別是針對物件的設計而有動態的行為,所以兩個概念相較之下 —— 根本是兩回事啊!”

通常類別會跟介面進行結合 —— 因為都是代表功能的規格與實踐,理所當然要討論類別跟介面合作時的推論與註記現象。既然筆者不鼓勵類別與型別合作,理所當然就會跳過這方面的說明~

繼承過後的類別之型別推論與註記 Type Inference & Annotation of Inherited Class

接下來筆者用簡單繼承過 Horse 的子類別,檢視子類別的型別推論機制。

另外,筆者會將 Horse 類別裡的 infoText 成員方法改成 protected 模式,為的就是讓子類別可以自創自己的 infoText 方法但是不被外部竄改。

程式碼如下:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614S9KbHoRd2h.png

Unicorn 這個子類別的結構應該對讀者來說簡單吧!(筆者就不貼 Unicorn 的圖 XDDDDDD,應該會讓人感到可愛又崩潰

  • Unicorn 繼承 Horse 時,除了 name 必須讓使用者自訂外,其他的父類別要求的參數都補上去了(super 可以看成父類別建構子函式
  • Unicorn 覆寫了父類別的 infoText 的成員方法
  • 因為獨角獸會吐彩虹色嘔吐物,所以你可以叫 Unicorn 類別建立的物件去呼叫 puke 這個成員方法喔(這是什麼設定?

我們來建一個 Unicorn 物件來看看推論出來的型別。(變數 aRandomUnicorn 推論結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614dSJMOVYHVY.png

https://ithelp.ithome.com.tw/upload/images/20190923/201206140EoFIotpC8.png
圖八:其實也不意外,一個 Unicorn 物件,不是 Unicorn 那會是啥呢 XD?

不過,你覺得這個 Case 成立嗎?

https://ithelp.ithome.com.tw/upload/images/20190923/20120614VDk8Ov5dyU.png

事實上是可以的,結果如圖九所示。

https://ithelp.ithome.com.tw/upload/images/20190923/20120614tHoqIXFfmi.png
圖九:UnicornHorse 的子類別,且 Unicorn 類別建構的物件可以被指派到被註記為父類別型態的物件

因此可以認定被註記為父類別的變數可以指派子類別的物件 —— 在原生 JS 裡的詮釋下 —— 它們隸屬於同一個原型鍊(Prototype Chain)下的產物。

但是,有註記與沒註記父類別仍然有差,而且差得可能比讀者想像中多!

筆者特別為 Unicorn 新增 puke 方法不是沒緣由的,我們來看以下程式碼被 TypeScript 檢測的狀況。(姐測結果如圖十;錯誤訊息如圖十一)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614BKP8IBEFiJ.png

https://ithelp.ithome.com.tw/upload/images/20190923/20120614O2KnBgdwCW.png
圖十:結果被註記為 Horse 的變數,“踏破鐵鞋無覓處、不能恣意亂嘔吐” —— 簡直天地不容啊!

https://ithelp.ithome.com.tw/upload/images/20190923/20120614Tv6t28H8kg.png
圖十一:因為 TypeScript 參考的是 Horse 類別而不是 Unicorn 類別的成員,因此才會被 TS 警告

從這裡得知一個很重要的點:儘管被父類別註記的變數可以接收子類別建構的物件,但是子類別新增了父類別沒有的成員,該成員若被呼叫時 —— 會被 TypeScript 警告

但筆直還沒討論完,因為還有一個看似不起眼但會讓讀者感到怪的特性 —— 子類別有機會可以代表父類別型別

“WTx!作者你是指以下這樣的情形嗎?”

https://ithelp.ithome.com.tw/upload/images/20190923/20120614NTLdf7XtyB.png

首先,以上的程式碼 —— 一定會錯!但是錯誤訊息筆者用幾個字形容:“很有意思~”。

請看以下的結果!(圖十二為錯誤訊息)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614SWnX6VpQnP.png
圖十二:筆者剛開始以為 UnicornHorse 的子類別,因此不能代表 Horse,不過這錯誤訊息到底在暗示什麼?!

這錯誤訊息透漏出一些很細節的概念:

Property 'puke' is missing in type 'Horse' but required in type 'Unicorn'.

恩!很有趣的一個問題 —— 如果父類別再額外定義 puke 方法就可以代表 Unicorn

如果是在父類別建構出來的物件 —— 手動對該物件新增屬性或方法就會破壞掉物件的完整性,一定會被 TypeScript 開罰單!

另外,筆者根據上面的錯誤訊息,提出的回應是:

如果子類別 Cinherited 繼承父類別時,並沒有額外定義出更多成員,亦或者是只有覆蓋父類別的成員但沒有做其他事情,那麼是不是代表父類別創建出來的物件型別等效於該子類別 Cinherited

於是筆者快速建構一個子類別 Stallion 並直接對 Horse 繼承,但沒有做任何其他的事情:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614tZnEYE5oCY.png

我們來測測看以下的程式碼。(結果如圖十三)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614OIqcSIav89.png

https://ithelp.ithome.com.tw/upload/images/20190923/20120614wvSSRhhFfr.png
圖十三:哇塞!沒有錯誤,儘管積極註記為 Stallion,但是指派它的父類別 Horse 竟然也是通過的!

因此筆者把剛剛展示過的東西寫成一個重點供讀者做筆記用:

重點 3. 類別繼承之型別推論與註記

假設宣告某類別 C,另外再宣告 C_Inherit 為繼承 C 的子類別,則:

  1. 子類別 C_Inheirt型別推論機制跟普通類別的型別機制一模一樣(查看本篇重點 2)
  2. 若變數 A 被註記之型別為父類別 CA 除了可被指派 C 類別建構出來的物件外,子類別 C_Inherit 建構出來的物件也可以被指派到 A
  3. 若變數 B 被註記之型別為子類別 C_Inherit —— 在 C_Inherit 繼承父類別 C 的過程中,並未額外定義 C 本身沒有的成員的條件下,父類別 C 所建構出來的東西可以被指派到變數 B

型別等效假說 Typed Equivalence Hypothesis

貼心小提示

這個標題是筆者自立的,並不是參考任何 Document 或者是外來網站,讀者只會在本系列看到這個假說。

另外,物件完整性理論也是筆者自己在本系列訂立的,為的是解說方便,將一大坨概念抽象化的結果。

所以你幾乎不會在外面的 TypeScript 系列看到這些名詞。

從以上的範例,筆者又要大膽地再立一個假說:類別的型別判定原則與判定類別的成員格式沒兩樣 —— 所以兩個不同類別建造出來的物件,其物件型別的判定與類別名稱無關,只要格式相同就算通過

類別繼承過後的推論機制,不是以原型鍊(Prototype Chain)或者是類別名稱來辨識,而是以類別創造出來的物件格式判定是否為同一型別

(哎呀... 有點繞口令,讀者看不懂的話~可以看以下的範例)

想要真的讓以上那句話成立,我們要講一個更極端的案例 —— 某兩個類別的宣告僅僅只是名稱不同但是成員結構等等都相同,那麼型別是否會間接等效

https://ithelp.ithome.com.tw/upload/images/20190924/201206143mfhch7dH0.png

以上的 C1C2 類別都只有 public 模式的成員。我們來測試看看下面會不會通過。(測試結果如圖十四)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614zqshuP0OgE.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614KGHBebj6Mo.png
圖十四:筆者簡直要吐血啦~還真的通過!

所以意思是說:只要結構相同就會通過嗎!?

當然~(哦哦哦~所以是ㄧ樣囉?)~~~~~~ 答案不是這樣,請看以下的範例(錯誤訊息如圖十五):

https://ithelp.ithome.com.tw/upload/images/20190924/201206148nCtdhit47.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614PdTxRnWOzY.png
圖十五:結果 private 模式因為可以被自訂任意的行為,因此被 TypeScript 判定型別格式不等

因為 private 模式下的成員會使得類別的使用形式被改變,因此不能夠等效於其他格式相同的類別呢!看來剛剛的假說是有條件的~

此外,如果讀者測試改成 protected 模式,也會出現類似的錯誤訊息喔!

重點 4. 類別型別等效理論

若宣告兩個類別 C1C2 —— 其中 C1C2成員皆為 public 模式,並且所有的成員名稱對應型別皆相同,TypeScript 判定 C1 型別等效於 C2 型別。

讀者試試看

其實,根據類別等效理論的結果可以再反推 —— 連型別跟介面,只要格式一模ㄧ樣,都可以被等效。以下寫幾個範例讓讀者研究一下,看看這些程式碼能不能動作吧!

https://ithelp.ithome.com.tw/upload/images/20190924/20120614AOuoJu4GvT.png

小結

今天已經完整講完整個類別的推論與註記機制~

若類別跟介面結合的話,會產出什麼樣的型別推論機制呢?我們就留到下一篇來看看囉!


上一篇
Day 23. 機動藍圖・私有建構子 X 單身狗模式 - Private Constructor & Singleton Pattern
下一篇
Day 25. 機動藍圖・類別與介面 X 終極的組合 - Ultimate Combo of Class & Interface
系列文
讓 TypeScript 成為你全端開發的 ACE!51

1 則留言

0
turtle0617
iT邦新手 5 級 ‧ 2019-10-28 08:42:57

這個應該是錯字吧 XD

圖十三:哇塞!沒有錯誤,儘管積極註記為 Stallion,但是指派它的覆類別 Horse 竟然也是通過的!

太感謝~~ 已修改!/images/emoticon/emoticon12.gif

我要留言

立即登入留言