本系列第二部分:《機動藍圖》(The Agile Blueprint)篇章涵括的範圍就是 TypeScript 的重頭戲。不外乎,筆者想像中的內容大致上有這些(其實有些是筆者臨時打稿時想到可以講的部分XD),內容順序有可能會跳,不過還是給讀者稍微看過:
union
& intersection
“Favor object composition over class inheritance.”
(讀者請不要誤會,筆者不是說這句話錯,這句話很對!但是被誤解到錯得很離譜,甚至還出現各種 Antipattern 讓筆者覺得不講也不行。)
原本還打算把 Generics 通用型別塞進本篇章,但回過頭來,除了感覺又會過大外,筆者認為在本章節系列內應該要好好把 TypeScript Interface 與 Class 部分講完,進度太快也會吃不消。
因此我們來進行本篇章第一篇,正文開始!
在《前線篇章》系列,typescript-tutorial/01-basic
資料夾裡的 index.ts
內的程式碼有點過多。因此我們在新建另一個環境。開啟終端機進到 typescript-tutorial
裡面,並且建置新的資料夾(命名不限):
$ cd PATH_TO/typescript-tutorial
$ mkdir 02-interface-class
讀者應該熟悉了 TypeScript 的指令,一樣初始化 TS 編譯器設定檔:
$ tsc --init
先打開編譯器設定檔 —— 也就是 tsconfig.json
,把 strictNullChecks
這個選項設定為 true
(如圖二),筆者忘記在《前線維護》篇章寫到這句話,這是一個說小並不小,說大影響也不太大 —— 但依然會影響到後續文章的寫法。使用 VSCode 開啟 typescript-tutorial
這個資料夾你應該會看到類似這個畫面。(如圖一)
圖一:新建 02-interface-class
的資料夾
圖二:將 tsconfig.json
裡的 strictNullChecks
改成 true
如果都已經建置完畢後,新增 index.ts
檔案在 typescript-tutorial/02-interface-class
的資料夾裡然後就開始囉~
讀者還記得型別化名(Type Alias)是什麼嗎? 如果還不記得的話,讀者請參見 Day 08. 的文章囉~不過應該還算簡單,筆者就二話不說把它的重點搬過來:
《前線維護・明文型別 X 格式為王》之 重點 2. 型別化名 Type Alias
若某型別
T
,T
可為任何的型別(包含原始型別、物件型別、TypeScript 內建型別、明文型別、複合型別、Generics 通用型別等)。其中我們想要讓該型別T
等效於別名A
,則可以使用 TypeScript 的type
關鍵字進行化名宣告:type A = T;
型別化名的主要目的為簡化程式碼以及進行型別的抽象化(Type Abstraction)
型別化名的意義就是把複雜格式(尤其是明文格式)的型別進行程式碼簡化與抽象化 —— 抽象化的概念一再地出現,非常重要呢!
而 TypeScript 的介面(Interface)與型別化名的作用有些相似,但可以把它想成更具彈性的型別。
不過呢,儘管型別化名是可以用原始型別與各種廣義物件格式表示,譬如:
貼心小提示
讀者可能覺得最後一個案例 —— 可以直接把字串的值當成一種型別並且
union
起來 —— 這也是明文型別(Literal Type)的一種形式。通常看到的明文型別是物件的格式,但實際上單純原始型別的值或廣義物件的值都可以獨立成一個型別,這部分之前沒講到,不過筆者認為知道有這個功能就好。
然而,介面(Interface)跟型別化名可就不同了。
它只能以兩種形式以及混合的方式(Hybrid)呈現。筆者刻意把混合部分隔離出來,因為最後一種方式只是把前兩種形式進行混合罷了。
重點 1. TypeScript 介面(Interface)的定義與種類
TypeScript Interface 可以藉由關鍵字
interface
宣告出來,介面裡面的詳細定義可為:
- 物件格式:即 JSON 格式,是為屬性對型別,不是對值
- 單一函式格式:沒有任何屬性,就是函式而已,但不一定需要標上函式名稱
- 混合格式:即『物件格式』與『單一函式格式』混合在一起
然而,在物件格式的介面定義下 —— 剛剛筆者在貼心小提示所講的 —— 如果你真的把值(比如字串當型別)寫下去,也是可以的!只是以後你註記某變數時使用了該介面,該變數必須強制把該屬性對應的值原封不動複製上去。
而單一函式格式與混合格式會在後續進行補充。
但我們先看看前兩種版本的介面定義的形式,參見以下的範例。
可以看出 UserInfo
把各種屬性對應的型別都描述出來了,很像在編列資料格式的型態。(筆者知道這是廢話XD)
然而,UpdateRecord
描述的單純就只有一個函式,它必須傳入的參數 —— 各自符合 number
型別以及 UserInfo
介面的參數進去。而該函式的輸出狀態為不輸出(也就是 void
)。從這裡就可以看出,我們可以藉由 UpdateRecord
的介面定義一系列的修改 UserInfo
的函式,比如說更改名稱、更改性別、更改興趣等等。
筆者用一些例子展示給讀者看,註記介面的各種狀況。(以下程式碼檢測結果如圖三,錯誤訊息分別為圖四~圖六)
圖三:跟 type
作的型別化名概念很像,多一鍵、少一鍵與屬性對應型別錯誤都會錯
圖四:少一鍵 age
就被 TS 警告
圖五:job
沒有存在在 Person
這個介面裡,因此多一鍵也會被 TS 警告
圖六:hasPet
屬性必須接收 boolean
,型別錯誤會被警告,不過感覺被警告也是挺正常
我們還有另外一種案例要測,那就是將變數或 JSON 物件格式傳入函式作為參數的案例。還記得在 Day 08. 裡我們檢測到狹義物件的明文格式作為參數的狀況嗎?上一次我們得出的最後結論是 —— 只要我們出現廣義物件時,會建議進行註記的動作,不然沒有註記過後的變數,帶入函式的參數,被檢測的限制會變輕。
以下就是要測測看 Interface 有沒有類似的狀況。(TypeScript 檢測結果如圖七;錯誤訊息如圖八)
圖七:結果如果直接將物件的明文形式作為參數代入會被警告;然而,沒有對變數作積極註記,但看起來跟 Person
很像,就算通過
圖八:恩,TypeScript 確實認為這個物件的明文格式比 Person
介面所定義的屬性多了一些,因此被警告
如果讀者仔細比對本篇用 interface
的結果與使用 type
的結果,幾乎完全一樣。
重點 2. 為註記之變數作為函式參數的行為
由於可以藉由存取物件的表現形式在某變數裡 —— 其中該變數沒有被積極註記 —— 只要該變數至少有符合介面的格式,依然可以通過函式參數對於變數的值的驗證。
但若想避免此狀況發生,任何變數需要存取物件時,必須進行積極註記型別或介面的動作。
讀者可能會非常納悶(連筆者剛開始學也是一樣)。
“那
type
跟interface
到底是差在哪裡啊?感覺都沒差啊!”
偷偷說一下,其實用法上感覺沒差多少,但意義上的差別可就超級大了啊!
你可以同時使用 type
或 interface
,不過這樣交錯下來可能會導致剛入門 TypeScript 的人會有好像都可以亂用都沒差的錯覺。
因此筆者會把這個問題的探討慢慢呈現出來,今天先把 interface
這東西給讀者瀏覽過一次所有狀況。儘管本篇就算只有兩個大重點,但後面的探討只會越來越深入,開頭篇章先把基礎暖身做好再說~