閱讀本篇文章前,仔細想想看
- 大致上已經了解類別的基本用法與性質了嗎?
- TypeScript 針對物件方面的型別推論與註記機制為何?
如果還沒理解完畢的話,可以先翻看以下的文章喔:
- Day 02. 前線維護・型別推論 X 註記 - Type Inference & Annotation
- Day 18. 類別宣告 X 藍圖設計 - TypeScript Class
- Day 19. 存取修飾 X 藍圖規劃 - TypeScript Class Access Modifiers
- Day 20. 類別繼承 X 延用設計 - TypeScript Class Inheritance
- Day 21. 靜態成員 X 即刻操作 - Static Properties & Methods
- Day 22. 機動藍圖・特殊成員 X 存取方法 - TypeScript Class Accessors
筆者事實上是因為寫類別這個主題過程太投入,差點忘記還要講類別的推論與註記過程。XD
所以我們趕緊就正文開始吧!
基本上,讀者看到今天的文章,應該已經對型別的推論與註記概念很了解。如果是跳到後面看到本篇文章時,建議可以先看筆者一開始列的文章列表喔,其中《前線維護》篇章主要是讓讀者能夠理解 Type Inference 與 Annotation 的定義與機制 —— 因為這可是 TypeScript 的主打 Feature 呢!
以下筆者就派出本日的範例類別 Horse
:
(筆者知道這個類別範例很白癡,將就一下XD)
首先,以上的類別 Horse
有四個基本成員變數(Member Variables),分別代表:
name
:馬的名稱,字串型態,而且可以供外部使用(public
模式)color
:馬的顏色,設定為列舉型態,顏色也可以被外面竄改type
:馬的種類,為字串型態;儘管為 public
模式但卻被標記 readonly
—— 唯讀模式noise
:馬的叫聲,但是是 private
模式,而且有預設值(讀者會學這種 'MeeeeeeeEeeééeéeée~'
的叫聲嗎?)(以上的說明,筆者真的沒有在罵粗話 XD)
再來也有幾個成員方法(Member Methods):
makeNoise
是 public
模式,負責讓馬叫(info
也是 public
模式,負責印出馬的基本資料infoText
則是 private
模式,負責將馬的基本資料用 Template String 湊合起來理解之後,今天的目標是要理解類別的型別推論與註記機制。
因此我們先變出一隻馬,但是完整建構馬之前:筆者必須截下這張圖。(建構物件時 VSCode 提醒視窗如圖一)
圖一:還沒建構物件前,TypeScript 提醒我們 Horse
的建構子函式的參數被推論的結果
很明顯看到:
Horse(name: string, color: Color, type: string, noise?: string): Horse
這是屬於函式型別推論結果,光是這一行就已經告訴我們很多很有用的訊息呢!
Horse
—— 也就是說宣告類別(Class)就相當於宣告一個函式 —— 原生 JS 在模擬類別的情形時,就是用 Function 來模擬的。Horse
的回傳型別是 Horse
,代表類別本身是一種型別化名(Type Alias);也就是說,宣告一個類別等於建造新的型別,因此也印證筆者之前在類別繼承篇章偶然提到的概念:“類別與介面跟普通的型別一樣,都是型別化名的一種”。name: string
,TypeScript 提示我們 —— 第一個參數要填入參數型態為 string
,代表 name
這個參數根據剛剛筆者講的第 3 點,假設我們已經填好第一個參數,那麼下一個畫面出現的是(如圖二):
圖二:第一個參數填完,自動提示你第二個參數為 color: Color
以上是筆者想要提醒讀者 —— 要好好善用 TypeScript 提供給你的工具,增進開發效率的小細節不要輕易放過。
另外,有時候注意到工具提供的訊息 —— 可以猜出一些 TypeScript 隱藏的機制,很方便呢!等等筆者繼續驗證類別的註記部分,基本上又會再把 Day 03. 提到的完整性理論再次搬出來喔。
首先先產出一隻小馬:
ㄧ樣來看看類別建造過後的物件被推論的結果是什麼。(推論結果如圖三)
圖三:aRandomHorsie
的推論結果是 Horse
型別
恩,筆者來下一個很根本(但很廢話)的重點。
重點 1. 類別型別
宣告新的類別 —— 本身就是在創造新的型別化名;也就是說,我們可以使用類別名稱作為變數的型別註記(Type Annotation)。
根據很久之前描述的廣義物件完整性原則:
Horse
型別的變數的值,必須用 Horse
類別建構出來的物件進行完美覆蓋動作以下的程式碼就是對以上的案例的驗證行為。(驗證結果如圖四;錯誤訊息如圖五~圖七)
圖四:各種層面的出錯,TypeScript 會主動河蟹你
圖五:難道紅色就錯了嗎?
圖六:難道豢養的馬也錯了嗎?
圖七:難道馬不見了也是我的錯,開什麼玩笑?
以上就是驗證過後的類別推論行為的機制,也符合完整性原則。
重點 2. 類別的型別推論機制 Type Reference in Class
若變數被指派的值為類別
C
建構出來的物件,則 TypeScript 會自動推論該變數之型別為C
。被推論出型別為
C
的變數符合廣義物件完整性原則:
- 該變數不能夠新增屬性
- 該變數在原有屬性下不能指派錯誤的型別的值
- 要完整覆寫該變數,指派的值必須是類別
C
建構出來的物件
(恩,用 <-- 這是 P 話不要理會筆者)of
作為副標的介系詞應該會比 in
適合
型別註記其實沒什麼好講的,就是以下幾種註記方式:
這邊就留給讀者自己試試看囉。
哦哦哦哦~~接下來才是很整人的部分 —— 我們要討論的 Case 很多,不光只有討論單純類別的型別推論與註記行為,這樣交代實在是太不負責任啦~~~XD。(笑 P 笑)
筆者要討論的狀況有這些:
類別跟介面想當然會有 4 種組合。不過呢,類別也可以結合普通型別 —— 實踐 type
宣告的型別化名裡的靜態資料格式,硬要討論總共有 8 種組合(但絕對不會是八大行業)。
所以事實上,你也可以這麼做,這是之前沒提到的:
type SomeType = {
message: string;
}
class SomeClass implements SomeType {
/* ... 略 */
}
然而,筆者不鼓勵類別與型別 type
結合的原因還是一句話:“型別代表的意義是靜態資料格式,類別是針對物件的設計而有動態的行為,所以兩個概念相較之下 —— 根本是兩回事啊!”
通常類別會跟介面進行結合 —— 因為都是代表功能的規格與實踐,理所當然要討論類別跟介面合作時的推論與註記現象。既然筆者不鼓勵類別與型別合作,理所當然就會跳過這方面的說明~
接下來筆者用簡單繼承過 Horse
的子類別,檢視子類別的型別推論機制。
另外,筆者會將 Horse
類別裡的 infoText
成員方法改成 protected
模式,為的就是讓子類別可以自創自己的 infoText
方法但是不被外部竄改。
程式碼如下:
Unicorn
這個子類別的結構應該對讀者來說簡單吧!(筆者就不貼 Unicorn 的圖 XDDDDDD,應該會讓人感到可愛又崩潰)
Unicorn
繼承 Horse
時,除了 name
必須讓使用者自訂外,其他的父類別要求的參數都補上去了(super
可以看成父類別建構子函式)Unicorn
覆寫了父類別的 infoText
的成員方法Unicorn
類別建立的物件去呼叫 puke
這個成員方法喔(我們來建一個 Unicorn
物件來看看推論出來的型別。(變數 aRandomUnicorn
推論結果如圖八)
圖八:其實也不意外,一個 Unicorn
物件,不是 Unicorn
那會是啥呢 XD?
不過,你覺得這個 Case 成立嗎?
事實上是可以的,結果如圖九所示。
圖九:Unicorn
為 Horse
的子類別,且 Unicorn
類別建構的物件可以被指派到被註記為父類別型態的物件
因此可以認定被註記為父類別的變數可以指派子類別的物件 —— 在原生 JS 裡的詮釋下 —— 它們隸屬於同一個原型鍊(Prototype Chain)下的產物。
但是,有註記與沒註記父類別仍然有差,而且差得可能比讀者想像中多!
筆者特別為 Unicorn
新增 puke
方法不是沒緣由的,我們來看以下程式碼被 TypeScript 檢測的狀況。(姐測結果如圖十;錯誤訊息如圖十一)
圖十:結果被註記為 Horse
的變數,“踏破鐵鞋無覓處、不能恣意亂嘔吐” —— 簡直天地不容啊!
圖十一:因為 TypeScript 參考的是 Horse
類別而不是 Unicorn
類別的成員,因此才會被 TS 警告
從這裡得知一個很重要的點:儘管被父類別註記的變數可以接收子類別建構的物件,但是子類別新增了父類別沒有的成員,該成員若被呼叫時 —— 會被 TypeScript 警告。
但筆直還沒討論完,因為還有一個看似不起眼但會讓讀者感到怪的特性 —— 子類別有機會可以代表父類別型別!
“WTx!作者你是指以下這樣的情形嗎?”
首先,以上的程式碼 —— 一定會錯!但是錯誤訊息筆者用幾個字形容:“很有意思~”。
請看以下的結果!(圖十二為錯誤訊息)
圖十二:筆者剛開始以為 Unicorn
為 Horse
的子類別,因此不能代表 Horse
,不過這錯誤訊息到底在暗示什麼?!
這錯誤訊息透漏出一些很細節的概念:
Property 'puke' is missing in type 'Horse' but required in type 'Unicorn'.
恩!很有趣的一個問題 —— 如果父類別再額外定義 puke
方法就可以代表 Unicorn
嗎?
如果是在父類別建構出來的物件 —— 手動對該物件新增屬性或方法就會破壞掉物件的完整性,一定會被 TypeScript 開罰單!
另外,筆者根據上面的錯誤訊息,提出的回應是:
如果子類別
Cinherited
繼承父類別時,並沒有額外定義出更多成員,亦或者是只有覆蓋父類別的成員但沒有做其他事情,那麼是不是代表父類別創建出來的物件型別等效於該子類別Cinherited
呢?
於是筆者快速建構一個子類別 Stallion
並直接對 Horse
繼承,但沒有做任何其他的事情:
我們來測測看以下的程式碼。(結果如圖十三)
圖十三:哇塞!沒有錯誤,儘管積極註記為 Stallion
,但是指派它的父類別 Horse
竟然也是通過的!
因此筆者把剛剛展示過的東西寫成一個重點供讀者做筆記用:
重點 3. 類別繼承之型別推論與註記
假設宣告某類別
C
,另外再宣告C_Inherit
為繼承C
的子類別,則:
- 子類別
C_Inheirt
的型別推論機制跟普通類別的型別機制一模一樣(查看本篇重點 2)- 若變數
A
被註記之型別為父類別C
,A
除了可被指派C
類別建構出來的物件外,子類別C_Inherit
建構出來的物件也可以被指派到A
- 若變數
B
被註記之型別為子類別C_Inherit
—— 在C_Inherit
繼承父類別C
的過程中,並未額外定義C
本身沒有的成員的條件下,父類別C
所建構出來的東西可以被指派到變數B
貼心小提示
這個標題是筆者自立的,並不是參考任何 Document 或者是外來網站,讀者只會在本系列看到這個假說。
另外,物件完整性理論也是筆者自己在本系列訂立的,為的是解說方便,將一大坨概念抽象化的結果。
所以你幾乎不會在外面的 TypeScript 系列看到這些名詞。
從以上的範例,筆者又要大膽地再立一個假說:類別的型別判定原則與判定類別的成員格式沒兩樣 —— 所以兩個不同類別建造出來的物件,其物件型別的判定與類別名稱無關,只要格式相同就算通過
類別繼承過後的推論機制,不是以原型鍊(Prototype Chain)或者是類別名稱來辨識,而是以類別創造出來的物件格式判定是否為同一型別!
(哎呀... 有點繞口令,讀者看不懂的話~可以看以下的範例)
想要真的讓以上那句話成立,我們要講一個更極端的案例 —— 某兩個類別的宣告僅僅只是名稱不同但是成員結構等等都相同,那麼型別是否會間接等效?
以上的 C1
與 C2
類別都只有 public
模式的成員。我們來測試看看下面會不會通過。(測試結果如圖十四)
圖十四:筆者簡直要吐血啦~還真的通過!
所以意思是說:只要結構相同就會通過嗎!?
當然~(哦哦哦~所以是ㄧ樣囉?)~~~~~~ 答案不是這樣,請看以下的範例(錯誤訊息如圖十五):
圖十五:結果 private
模式因為可以被自訂任意的行為,因此被 TypeScript 判定型別格式不等
因為 private
模式下的成員會使得類別的使用形式被改變,因此不能夠等效於其他格式相同的類別呢!看來剛剛的假說是有條件的~
此外,如果讀者測試改成 protected
模式,也會出現類似的錯誤訊息喔!
重點 4. 類別型別等效理論
若宣告兩個類別
C1
與C2
—— 其中C1
與C2
的成員皆為public
模式,並且所有的成員名稱對應型別皆相同,TypeScript 判定C1
型別等效於C2
型別。
讀者試試看
其實,根據類別等效理論的結果可以再反推 —— 連型別跟介面,只要格式一模ㄧ樣,都可以被等效。以下寫幾個範例讓讀者研究一下,看看這些程式碼能不能動作吧!
今天已經完整講完整個類別的推論與註記機制~
若類別跟介面結合的話,會產出什麼樣的型別推論機制呢?我們就留到下一篇來看看囉!
這個應該是錯字吧 XD
圖十三:哇塞!沒有錯誤,儘管積極註記為 Stallion,但是指派它的
覆類別
Horse 竟然也是通過的!
太感謝~~ 已修改!