iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
2
Modern Web

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

Day 15. 機動藍圖・功能多樣性 X 多樣性介面 - More on TypeScript Interface

https://ithelp.ithome.com.tw/upload/images/20190925/20120614afwBQE5jEn.png

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

  1. 到目前為止對於 TypeScript Interface 介面的理解到什麼程度呢?
  2. 你認為 TypeScript 和第三方套件/專案/框架協作上會有什麼困難點?(本系列後續文章還會探討更多有關的議題喔!)

如果還沒理解完畢的話,可以先翻看前一篇文章喔!

本日正文前的廢文稍微多一些,請讀者多多諒解~

哇,這已經是第 15 篇了! —— 恩... 本系列文原本打算讓讀者 30 天內速成 TypeScript;不過後來筆者轉換方向,改成以推讀者入坑 TypeScript 為目標(或者不一定用到 TS 但至少從中學到東西),因此才寫作本系列 —— 不過每篇文長也是有些長,不知道有沒有起到效果,寫作過程中也是感到迷茫與不確定的時候。

當初開始寫這篇系列文前,原本沒有想過要入賽。筆者原本是想學 Angular 這個框架,不過後來看到 Angular 是用 TypeScript 作為主流開發語言,心想可能要學習一下 TypeScript

後來不知道什麼原因,覺得 TypeScript 實在是太有趣,於是爬滿整個文件跟其他 TypeScript 領域的神人們的教學影片與文章,結果不知不覺兩週過去,偶然看到鐵人賽的報名資訊

然而,看到鐵人賽的報名時,也已經是末尾(差不多剩下一週可以報名),所以才非常緊迫地訂出草稿,並且開始趕文章。打完草稿後丟出來,完蛋 —— 範圍太大啊!啊!啊!啊!啊!啊!超級悲劇!

積極撰文的同時,前十四天的文章也再逐步改善中。(主要是文法部份,本人國文造詣超爛,口語多到炸,連接詞用得很悲劇)前七到八天的文章,筆者重看覺得很丟臉 —— 發文前一天會強迫自己重新審文,每天下午發文一定在按下送出的按鈕前重新看一遍,能夠刪減冗言贅字就刪、換句話說更通順就換。

如果筆者能夠提早看到鐵人賽時間,並且提早準備,應該還可以刪減內容。不過後來想說算了 —— 寫就寫,鐵人賽頂多就這段期間。XDDDD

貼心小提示

要參賽的讀者請不要衝動,三思而後行;但是也可以學筆者這樣 —— 不入虎穴、焉得虎子,搞不好可以教出老虎五隻?

[2019.09.25. AM 00:03 新增訊息]
本系列從 Day 12. 開始到鐵人賽完整 30 天,確定會是《機動藍圖》系列篇章 —— 後續的《戰線擴張》系列會以延長賽方式進行!

讀者不要認為作者只講到一半就不寫了~(俗稱射後不理)

本系列作者不是落跑作者啊!!!!!(好歹本系列 30 天後完成趴數有達到 6% 以上!哈!哈!哈!哈!哈!)XDDDDDDD

好了,今天應該也會講得很輕鬆~(希望不要再像昨天一樣,講到後面扯到專案開發上的協作問題,想收尾也難收)

正文開始

更多介面的功能

貼心小提示

由於本篇已經是本系列的中間篇章,可能有新的讀者(誤入),因此筆者再進行名詞解釋:

  • 狹義物件指得是單純 JSON 物件({} 的格式)
  • 廣義物件指得是包含狹義物件外,還有陣列、函式、類別以及類別建造的物件等等

模仿部分廣義物件的行為 - Indexable Types

“恩... 介面不都是只有狹義物件(也就是 JSON 物件)的表現形式嗎?難道其他物件的格式(例如:陣列)也可以被模仿?”

筆者這邊是以個人見解來表示:TypeScript 的介面以及型別都有 —— 針對物件的屬性或索引做更神奇的微調,這個功能稱之為 Indexable Types(中文好難翻:可控索引型別?筆者還是不要亂翻譯好了),它的寫法是這樣:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614qdQvSqYp3C.png

我們先來看第一個例子,也就是 Dictionary 這個型別。

[propName: string]: T 的意思是 —— 只要屬性為字串型別,其對應的值之型別必須要為 T。以 Dictionary 的例子來看,這裡的 T 是字串型別 string;也就是說,任何字串型的索引只能接字串型態的值。

以下舉幾個使用 Dictionary 的狀況(以下程式碼被 TypeScript 檢測結果如圖一)。

https://ithelp.ithome.com.tw/upload/images/20190917/20120614OuZeYQJm1H.png

https://ithelp.ithome.com.tw/upload/images/20190917/20120614mkJvdr6E2z.png
圖一:基本上,只要屬性對應的值非字串的話,就會被 TypeScript 警告

由以上結果得知,藉由這種方式可以讓使用者任意新增屬性,但對應的值必須被鎖定在特定的型別(Dictionary 為例,就被鎖在 string 這個型別)。

再來是第二個例子的使用情況 —— StringTypedList 這個介面,但它是使用 [index: number] —— 也就是屬性為數字型態。(檢測結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20190917/20120614GJx9xNvkmf.png

https://ithelp.ithome.com.tw/upload/images/20190917/20120614USmd4klOd9.png
圖二:StringTypedList 檢測結果

這邊要特別注意的點有:

  • 不能直接用陣列形式初始化值(除非是空陣列),由於陣列屬於 JS 物件的一種,而物件的屬性初始化時不允許為 string 以外的型態,因此初始化陣列的索引(index)都會以 '0', '1', '2' ... 這種字串的數字形式初始化,所以才會被 TypeScript 判定結果是錯誤!(特別再把錯誤的部分截在圖三中)
  • 為何空陣列可以被初始化則是因為 TypeScript 認為都沒有屬性,沒有檢測之必要
  • [index: number] 將索引部分鎖定在 number 型別上,目的是為了防止開發者呼叫字串型別的屬性(或是用點的方式呼叫屬性),而是模擬陣列的行為 -- 用數字來檢索該物件裡的內容
  • 當物件必須在 [index: number] 這種狀態下初始化時,必須用 JSON 物件的格式,指定索引的位置(數值)並填入對應的值(當然,值必須符合型別 T,其中 T[index: number]: T 裡面的 T

https://ithelp.ithome.com.tw/upload/images/20190917/2012061441cGSRcpHw.png
圖三:儘管屬性專門接收數字型別,但卻不能直接初始化為陣列

另外,StringTypedList 的介面的實作有點類似雜湊表(Hash Table,又被稱為哈希表,直翻感覺有點怪怪)

重點 1. Indexable Types

若想限制物件索引為特定型別 —— 比如 numberstring,可以使用以下的格式。其中,Indexable Type 的實作可以在型別系統或介面出現

[keyName: TKey]: TValue 

其中,必須遵守下面的規則:

  • TKey 必須為 number 或者是 string 其中一種,不能為其他型別與 numberstring 的複合格式(連 number | string 是不接受的!)
  • TValue 可為任意型別

(由於 ES6 有新的 Symbol 型別出現,目前有在討論新增 Symbol 作為索引可以接受的型別)

讀者試試看

筆者提供一些沒辦法在短短篇幅內討論的問題,因此這裡給有興趣的讀者去想想:

  1. 如果知道 ES6 Symbol 這個功能的讀者,有沒有辦法實踐出 [keyName: Symbol]: TValue 這種形式?
  2. 這樣的寫法在 TypeScript 是可以被接受的嗎?
type UseBothKeyType = {
  [key: number]: T1,
  [key: string]: T2
};
  1. 這樣的寫法會不會出現問題?
type UserInfo = {
  name: string,
  [prop: string]: string;
}
  1. 承上題,那這樣的寫法會不會出現問題?
type UserInfo = {
  name: string,
  birth: Date,
  [prop: string]: string;
}

選用屬性 Optional Properties

在前幾篇的範例有展示過,這裡稍微提及:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614cRGoGh7y7Y.png

基本上,介面裡運用選用屬性跟型別裡運用的結果都差不多,不過這裡就照搬 Day 13. 探討過後的結果:

  • Interface 介面的意義是規格
  • Type 型別的意義則是靜態的物件型別格式

至於選用屬性在介面時的行為 —— 就由讀者自行發掘吧~基本上參照 Day 09. 選用屬性 Optional Properties 的文章,在介面時的效果也差沒多少呢。

唯讀屬性 Readonly Property

唯讀(Read-only)屬性標註非常簡單,就是在屬性前加入 readonly 的關鍵字,該屬性就會變成唯讀模式(Read-Only)。另外,這個功能型別系統也可以使用

https://ithelp.ithome.com.tw/upload/images/20190917/20120614WToQlxQg2w.png

以下簡單地測試一下。(TypeScript 檢測結果如圖四;錯誤訊息如圖五)

https://ithelp.ithome.com.tw/upload/images/20190917/20120614kJiLyvvmNY.png
圖四:硬想寫入含 readoly 的屬性,會被 TypeScript 阻止

https://ithelp.ithome.com.tw/upload/images/20190917/20120614FbRydkNeOh.png
圖五:TypeScript 會提醒你,email 屬性是不可以被覆寫的(property 'email' is read-only property

重點 2. 唯讀屬性 Read-Only Property

若某型別 T 或介面 I 有包含某屬性 P,其中屬性 P 前面有標註 readonly

type T = {
  readonly P: TAny;
};

interface I {
  readonly P: TAny;
}

則任何經由型別 T 創造出來或經由 I 實踐出來的廣義物件,這些物件的屬性 P 只能讀取不能進行覆寫。

介面的混合格式 Hybrid Type Interface

英文挺容易讓人誤解 - Hybrid Type 指的不是型別系統裡的一種 type 的表示方式,而是指介面的宣告方式可以用混合的方式呈現

還記得介面的宣告是哪兩種方式嗎?筆者就聞風不動把 Day 12. 裡的重點搬過來:

《Day 12. 介面宣告 X 使用介面》 之 重點 1. TypeScript 介面(Interface)的定義與種類

TypeScript Interface 的定義方式為,使用關鍵字 interface 而後接介面的詳細定義:

  • 物件格式:即 JSON 格式,是為屬性對型別,不是對值
  • 單一函式格式:沒有任何屬性,就是函式而已,但不一定需要標上函式名稱
  • 混合格式:即將『物件格式』跟『單一函式格式』混合在一起

以下舉計數器 Counter 的介面為範例。

https://ithelp.ithome.com.tw/upload/images/20190917/20120614FPBkdnQaV3.png

根據以上的程式碼,筆者進行簡單的實作:

https://ithelp.ithome.com.tw/upload/images/20190917/201206141MhFvvUIQK.png

將此程式碼進行 TypeScript 的編譯並且使用 node 執行過後如圖六。

https://ithelp.ithome.com.tw/upload/images/20190917/20120614wD1Ot075ew.png
圖六:結果順利地按照順序印出 585 這三個結果

儘管混合式的介面可以做出更多不同的物件形式,然而筆者認為這不太是個好方法。

如果忘記要把整個介面的屬性或方法實作出來的話 —— 譬如將 increment 方法拿掉。(TypeScript 檢測結果如圖七)

https://ithelp.ithome.com.tw/upload/images/20190917/201206144yokobCvdx.png

https://ithelp.ithome.com.tw/upload/images/20190917/20120614PpkJfC5Ntd.png
圖七:結果 TypeScript 沒有理人啊!

編譯過後沒有出錯,但上面的程式碼執行過程一定會出錯。(錯誤結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20190917/201206146M5sHfluEC.png
圖八:counter.increment 根本是未定義的狀態

讀者可能問說,為何這時候 TypeScript 沒辦法檢查這個錯誤呢?

筆者其實也搞不清楚這裡的註記與推論機制到底是什麼。之所以提及這個部分的主要目的是為了展示給讀者:“你可以使用介面的混合型態,不過使用起來可能會遇到這種問題 —— 忘記實踐出混合型態介面裡的屬性與方法”。

筆者目前也想不太到什麼情況下會用介面的混合型態(Hybrid Type Interface),因此沒花太多時間研究這個雷點。

混合型態的用法在官方有說明,但筆者仍然認為:“TypeScript Class 和 Interface,光是學好基本的 OOP 與設計模式就夠實用了!”

所以呢,這裡筆者就不下重點了~讀者自己瞧吧!呵!呵!呵!呵!呵!(有病)

小結

本日篇章主要是把介面的雜項部分補齊。讀者應該會發現,除了混合型態的介面以外,其他的 Feature 都可以在 typeinterface 使用,這會讓學 TypeScript 的初心者們覺得:介面跟型別系統好像都沒差的

下一篇就是要統整型別系統跟介面的比較!因為筆者基本上已經把該介紹的介面相關語法都介紹完了。

另外,讀者有沒有發現一件事情?從本系列開講到目前為止 —— 執行 TypeScript Compiler 的次數:只有 3 次

  • 第一次是開篇的 Hello World 範例
  • 第二次是 Day 07. 解釋 Enumerated Type 具有反射性時,編譯 TS 的結果進行討論,不是測試用喔!
  • 第三次是今天這一篇,用來展示混合型態的介面的簡單的程式碼

使用普通 JS 的狀況,大部分得執行過程式碼,才能根據 Error Stack 結果除 Bug。

相比起來,TypeScript 的好處就是:

“不需要經過編譯,TypeScript 編譯器就會靜態地提醒程式碼哪裡會有潛在的 Bug”

這裡當然不是指 TypeScript 一定比原生 JS 好,反倒是先把原生 JS 基礎熟練過後,學習 TypeScript 才會如魚得水。(筆者回想起學 JS 的經驗,過程也是蠻莫名其妙的 XD,有空再說)

回過頭來,想要讓上述 TypeScript 的優點發威也是需要使用者非常清楚哪些事情是在 TypeScript 可以做的。

只要非常~非常~清楚 TypeScript 的推論與註記機制基本上就已經免除六到八成的 Bug,其餘就是學習更多語法與應用,剩下兩成可能就是一些開發者需要注意的或真的是稀有的案例,連筆者腦袋也想像不到的狀況呢~

筆者認定:後面的文章講再好,《前線維護》系列篇章是入門 TypeScript 基礎中的基礎


上一篇
Day 14. 機動藍圖・函式超載 X 究極融合 - Function Overload & Interface Merging
下一篇
Day 16. 機動藍圖・介面與型別 X 混用與比較 - TypeScript Interface V.S. Type
系列文
讓 TypeScript 成為你全端開發的 ACE!51

2 則留言

0
YH
iT邦新手 5 級 ‧ 2019-10-29 15:59:56

我覺得口語很好呀XD很好懂
最近剛好也在研究ts...覺得博大精深...

感謝支持 XD
不過筆者才剛碰型別系統也不到兩三個月時間,也深感覺得 TS 真的是TNND大坑
/images/emoticon/emoticon37.gif

0
農夫
iT邦新手 5 級 ‧ 2021-01-15 17:43:47

請問圖三的部份是不是有筆誤呢?

interface 定義的值只能接收 string 型別

interface StringTypedList {
    [index: number]: string;
}

但圖三的範例,宣告時給了 number 會報錯應該是正常的吧?

let stringTypedArrayLiteral: StringTypedList = [1, 2, 3];

這邊宣告我嘗試給予了 string 沒有收到 TS 的報錯 XD

let stringTypedArrayLiteral: StringTypedList = ['a', 'b', 'c'];

是不是我有搞錯什麼了 XD

我要留言

立即登入留言