# Typescript 型別與介面 ###### tags: `Typescript` ## 壹、前線維護・型別推論 X 註記 - Type Inference & Annotation ### 一、 基礎型別(Types) ![](https://i.imgur.com/qsxPANb.png) - **原始型別 Primitive Types** 包含 **number**, **string**, **boolean**, **undefined**, **null**, ES6 介紹的 **symbol** 與時常會在函式型別裡看到的 **void** - **物件型別 Object Types** 但我個人還會再細分成小類別,但這些型別的共同特徵是 —— 從原始型別或物件型別組合出來的複合型態(比如物件裡面的 Key-Value 個別是 string 和 number 型別組合成的): - 基礎物件型別:包含 JSON 物件,陣列(Array<T>或T[]),類別以及類別產出的物件(也就是 Class 以及藉由 Class new 出來的 Instance) - TypeScript 擴充型別:即 Enum 與 Tuple,內建在 TypeScript - 函式型別 Function Types:類似於 (input) => (Ouput) 這種格式的型別,後面會再多做說明 - **明文型別 Literal Type** 一個值本身也可以成為一個型別,比如字串 "Hello world" —— 若成為某變數的型別的話,它只能存剛好等於"Hello world"` 字串值;但通常會看到的是 Object Literal Type,後面也會再多做說明 - **特殊型別** 筆者自行分類的型別,即 any、never(TS 2.0釋出)以及最新的 unknown 型別(TS 3.0釋出),讀者可能覺得莫名其妙,不過這些型別的存在仍然有它的意義,而且很重要,陷阱總是出現在不理解這些特殊型別的特性 - **複合型別** 筆者自行分類的型別,即 union 與 intersection 的型別組合,但是跟其他的型別的差別在於:這類型的型別都是由邏輯運算子組成,分別為 | 與 & - **通用型別** Generic Types:留待進階的 TypeScript 文章介紹,一種讓程式碼可以變得更加通用的絕招 ### 二、型別推論在原始型的運作 #### 1.型別推論 - TS 除了認得定義過後的變數外,也認得該變數型別是 Boolean,這個辨識推論型別的行為就是所謂的**型別推論**(Type Inference) ![](https://i.imgur.com/vCieknt.png) #### 2.Nullable Types - 但如果對 nothing 或 nothingLiterally 這兩個型別作確認的話,結果 TypeScript 會把它們推論成 any 型別,因為 null 或 undefined 被稱為 **Nullable Types**,被認為是 any 型別 ![](https://i.imgur.com/rIRg04U.png) - Nullable Types 會被認為是 any 型別,因此可以被指定為任何型態 ![](https://i.imgur.com/2ll0HA3.png) #### 3. TDZ (Temporal Dead Zone,暫時性死區) - 宣告一個變數後,在還沒指派值之前,用另外一個變數去接他,這時 TS 就會提出質疑。 ![](https://i.imgur.com/XIiIk7S.png) - 在未確定變數被正式丟入合法的值之前的這段空間,不能使用該變數。 ![](https://i.imgur.com/igX7Xnd.png) #### 4. 對遲滯性指派進行型別註記 ```javascript= let A: type; A = B as type; ``` ## 貳、物件型別 X 完整性理論 - Object Types Basics ![](https://i.imgur.com/08H3R5u.png) ### 一、基礎物件型別 #### 1. **JSON 物件格式** TS 會提醒你推論後的型別是什麼 ![](https://i.imgur.com/3r149DA.png) 就算是 nullable 和 undefine 的型別也會出來,而不是 any ![](https://i.imgur.com/xrl21Bq.png) #### 2. 型別覆蓋的例子 ```javascript= let info = { name: 'Maxwell', age: 20, hasPet: false, } let something = { null: null, define: undefined, } ``` - **屬性值被錯誤的型別插入/覆寫干擾** 型別錯誤,TS 會主動給予提示 ![](https://i.imgur.com/DOhVlcH.png) - **物件分別被正確或者是錯誤的物件格式整體複寫** 多一個鍵或少一個鍵都不行 ![](https://i.imgur.com/zCrOQEE.png) - 型別推導時就會把整個物件,包括 key-value 的型別都作定義 ![](https://i.imgur.com/NEWX9mj.png) - 主動註記物件型別時,會被標記為物件型別,因此只要是物件型別就可以付與值給它 ![](https://i.imgur.com/IsrNrna.png) - **直接對物件新增值** 新增或減少值會引起 TS 的警告 ![](https://i.imgur.com/rRlCR0A.png) - 結論 - 對物件內的屬性覆寫,其值的型別必須相等 - 對整個物件覆寫,格式必須完全相等 - 用以下三種形式新增物件是允許的 ```javascript= let nestedObject = { prop: 'Hello', child: { prop1: 12, prop2: false, } } let obj1 = {hello: 'world'} let obj2 = {...obj1, goodbye: 'Cruel world'} let obj3 = {hello: 'Another World'} let obj4 = Object.assign(obj3, { goodbye: 'Cruel World' }) ``` ### 二. 定義物件 let justAnObject1 = { hello: 'World' } let justAnObject2: object = { hello: 'World' } 定義物件的時候這兩種寫法, 1. **沒有主動註記物件型別**,第一個可以改寫裡面的hello的值 justAnObject1.hello = ‘WW’ 2. **主動註記物件型別**,第二個則是要**整個物件覆蓋掉**,然後連key值都可以改 = justAnObject = { goodbye: 'Cruel world', kk: 'Another'} 因為 TS 認為object 型別指的是任何 JS 物件(儘管格式不同)都可以被套入,但是不允許對該物件做細部微調 - 物件狹義的定義 典型的 {} 這種東西的寫法 - 物件廣義物件的定義 包含 JSON 格式的物件、陣列、函式、類別、類別創建出之物件 #### 1. 將狹義物件用廣義物件覆蓋 實驗證實,在物件主動標記型別的狀態下,廣義定義的物件是可以覆蓋的 ![](https://i.imgur.com/GXu6Kur.png) #### 2. 將廣義物件用狹義物件覆蓋 ![](https://i.imgur.com/tksmPKw.png) **廣義物件在被 TypeScript 推論的狀態下,屬性不能被任意新增或更改成其他型別** 能夠做的事情只有: - 全面覆寫,廣義物件的屬性對照型別格式也要完全對位 - 更改廣義物件本身就擁有屬性對應的值,其中:要帶入的值的型態必須對應到該屬性的型態 ## 參、函式型別 X 積極註記 - Function Types ![](https://i.imgur.com/jmqUjoh.png) ### 1. 函式註記- 函數參數 函數參數如果不標記爲何種型別,他只能是 any,如果 TS 不管輸入參數的型別。像下面的函式就會變成字串型別+數字型別 ```javascript= let addition = function(paras1, paras2) { return paras1 + paras2 } addition(1, '2') ``` 因此必須在函數的參數上加上型別 ```javascript= let addition = function(paras1: number, paras2: number) { return paras1 + paras2 } addition(1, 2) ``` ### 1. 函式註記-函式輸出的註記 在輸入型別確認後,TS 會自動幫我們推導出輸出型別為何 ![](https://i.imgur.com/TUlWQNx.png) 也可以自行註記型別 ![](https://i.imgur.com/G3C6FNH.png) - 如果輸出的型別為 any 該如何註記? ![](https://i.imgur.com/H8JjSDt.png) - 我們儘量不要讓變數被推導為 any 的型態。 ![](https://i.imgur.com/lNcZAEx.png) - 函式不回傳值的狀態 void ![](https://i.imgur.com/SH8dtqO.png) - 驗證小實驗 - 只有定義回傳為 undefine 時,卻沒回傳值,TS檢查不會通過 ![](https://i.imgur.com/U3ktTPK.png) ## 肆、陣列型別 X 型別陣列 - Array Types ![](https://i.imgur.com/u1M6H7u.png) ### 1.陣列型別 Array Types - 單一陣列型別 TypeScript 會按照開發者認為的邏輯,把這種元素(Element)皆為同型別 T 的陣列用 T[] 的方式表示。 ![](https://i.imgur.com/WAQ0KMO.png) - 混合陣列型別 ![](https://i.imgur.com/gnhsyO5.png) - 函式陣列型別 ![](https://i.imgur.com/YDi8aVj.png) - 一個陣列中擁有不同種類的型別 ![](https://i.imgur.com/Xp5dLtJ.png) ![](https://i.imgur.com/uEzHhkA.png) - 陣列中物件擁有不一致數量的屬性 ![](https://i.imgur.com/ALjFEuZ.png) ### 2. 定義變數的型別 如果要在陣列中加上一個 null,就得再定義陣列時使用 union 允許 null ![](https://i.imgur.com/TZsoBow.png) ## 肆、前線維護・陣列與函式 X 陣列與元組 - Array & Functions & Tuples ![](https://i.imgur.com/ZaiYaLN.png) ### 一、回呼函數 回呼函數一般情況下 TS 會幫你註記 ![](https://i.imgur.com/tugbCar.png) ### 二、陣列與元組 Arrays & Tuples - 相對於型別註記(string|Date)[],我們只知道陣列中有這兩個型別,但不能指定哪個位置必須放哪個型別。 因此 TS 用元組來做註記 ![](https://i.imgur.com/tVkF4Vd.png) - 運用型別化名註記元組型別 ![](https://i.imgur.com/faTj4yf.png) - 元組與陣列表示法 ![](https://i.imgur.com/6tTv0fV.png) - 不過元組的缺點就是:型別等同的兩個元素,儘管型別上是正確的,但是資料的意義上是不同的,就算被顛倒,TypeScript 也因為只會比對型別而不會進行警告 ![](https://i.imgur.com/MmDUG4u.png) 通常會建議資料儘量用 JSON 物件格式表示 ![](https://i.imgur.com/4Au3XwN.png) ## 伍、前線維護・列舉型別 X 主觀列舉 - Enumerated Types ![](https://i.imgur.com/YixmmhK.png) ### 一、列舉(Enumerate) 把變數的範圍限制在某些限制下進行存取並賦予其定義 - 一串東西的集合 - 同時具備**主觀上強烈的共通性(Similarity)**及**獨有特質(Uniqueness)** Ex: 星期幾、上下左右、交通工具... - 定義列舉時不需等號 ```typescript= enum WeekDay { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday } ``` - ts 將上面這段程式碼編譯的結果 ```javascript= var WeekDay; (function (WeekDay) { WeekDay["Sunday"] = "Sun"; WeekDay["Monday"] = "Mon"; WeekDay["Tuesday"] = "Tue"; WeekDay["Wednesday"] = "Wed"; WeekDay["Thursday"] = "Thu"; WeekDay["Friday"] = "Fri"; WeekDay["Saturday"] = "Sat"; })(WeekDay || (WeekDay = {})); ``` - 取用 WeekDay 的值會是 Enum -> ```typescript= let TGIF = WeekDay.Sunday console.log(TGIF) // TGIF 為 enum let Sunday = WeekDay[TGIF] console.log(Sunday); // Sunday 為 string 型別 ``` - 指向字串用法 ```typescript= enum Week { Sunday = "Sun", Monday = "Mon", Tuesday = "Tue", Wednesday = "Wed", Thursday = "Thu", Friday = "Fri", Saturday = "Sat" } // 編譯結果 ===> var Week; (function (Week) { Week["Sunday"] = "Sun"; Week["Monday"] = "Mon"; Week["Tuesday"] = "Tue"; Week["Wednesday"] = "Wed"; Week["Thursday"] = "Thu"; Week["Friday"] = "Fri"; Week["Saturday"] = "Sat"; })(Week || (Week = {})); ``` - 指向數字,自動遞增 ```typescript= enum JobGrade{ JuniorE8 = 8, JuniorE9, SeniorE10, SeniorE11, Other = 0, Guest } // 編譯結果 ===> var JobGrade; (function (JobGrade) { JobGrade[JobGrade["JuniorE8"] = 8] = "JuniorE8"; JobGrade[JobGrade["JuniorE9"] = 9] = "JuniorE9"; JobGrade[JobGrade["SeniorE10"] = 10] = "SeniorE10"; JobGrade[JobGrade["SeniorE11"] = 11] = "SeniorE11"; JobGrade[JobGrade["Other"] = 0] = "Other"; JobGrade[JobGrade["Guest"] = 1] = "Guest"; })(JobGrade || (JobGrade = {})); ``` ## 陸、明文型別 X 格式為王 - Literal Types ![](https://i.imgur.com/sSiAehP.png) 只要是表達廣義物件的格式或者是任意型別(包含原始型別的)複合組合(union 與 intersection 也算在內就隸屬於明文型別的範疇。 ![](https://i.imgur.com/N65oyYE.png) ![](https://i.imgur.com/VuQ6OYz.png) ### 一、型別化名 Type Alias 型別化名的主要目的為簡化程式碼以及進行型別的抽象化(Type Abstraction) ```typescript= // 原本的型別太過複雜 let powerOp: (n1: number, n2: number) => number = (n1: number, n2: number) => { return n1 ** n2 } // 加上 Type Alias 進行簡化 type MathOperator = (n1: number, n2: number) => number let powerOpTypeAlias: MathOperator = (n1: number, n2: number) => n1 ** n2 ``` - 函式型別的化名 任何變數 A 被型別化名 U 註記,則該變數被指派到的函式值,不需要積極註記,因為早在定義化名的時候,這個步驟就被做掉了 => 函式型別不用積極註記的第二種情況 ![](https://i.imgur.com/ETxZdeu.png) - 廣義物件的積極註記 ```typescript= type PersonInfo = { name: string, age: number, hasPet: boolean } function printInfo(info: PersonInfo) { console.log(info.name) console.log(info.age); console.log(info.hasPet); } printInfo({ name: 'Martin', age: 28, hasPet: false, hello: 'John', // ts 提示錯誤 nothingSpecial: null, }) ``` 如果再變數裡的物件,只要明文型別有符合,不要減少,就算多出鍵值對也能通過TS 因此**任何變數需要存取廣義物件時,必須進行積極註記型別的動作。** ```typescript= let infoAboutMartin = { name: 'Martin', age: 28, hasPet: false, hello: 'John', nothingSpecial: null, } printInfo(infoAboutMartin) // 通過TS ``` ## 柒、選用屬性 X 型別擴展 - Optional Properties ![](https://i.imgur.com/R0GBJeb.png) ### 一、 使用<prop>?選用屬性註記 當物件內的屬性,不是必須出現的狀況,可以使用 Optional Properties ![](https://i.imgur.com/tC5WUgX.png) - 若某屬性 P 屬於某物件的明文型別 的屬性之一,且該屬性對應的型別值為 T,而 A 是該明文型別的別名,則: ``` type A = { P?: T }; ``` 代表: 1. 選擇性地忽略 P 這個屬性。 2. 因為推論出來的結果會是以下的形式,因此也可以選擇寫出 P 屬性但填入 undefined 這個值: ``` { P?: T | undefined } ``` ### 二、複合類別 ![](https://i.imgur.com/rUzkxnz.png) ### 三、元組型別的選用元素 ![](https://i.imgur.com/pjxdxpQ.png) ## 捌、特殊型別 - Never Type ![](https://i.imgur.com/esKA2xp.png) - 無法跳脫出該函式或方法 - 出現例外結果中斷執行 - never 型別為所有型別的 subtype ![](https://i.imgur.com/rK16cKS.png) ## 玖、特殊型別 - Any 真的非不得已狀態下或者是快速測試下,可以使用 any。不過開發上,儘量不要用到 any 比較好;或者真不巧,遇到 any,也應當主動註記。 ### 一、出現時機 - **遲滯性指派 Delayed Iniitialization**:變數定義時,除了未加註記(Type Annotation)外,也沒有指派值或者被指派為 Nullable Types。 - **一般宣告下的函式參數**:一般被宣告的函式,其參數通常會直接被推論為 any,又被稱作 Implicit any 的情形。此狀況是少數會被 TypeScript 主動通報的 - **函式回傳之值**:有些實務上,型別無法確定,因此到最後只能將回傳值預設為 any(如:JSON.parse) - **未註記之空陣列**:沒有積極型別註記到的空陣列,其預設推論為 any[] - **跟 I/O 行為有關**:例如,從外部 CSV 檔案讀取表格行格式(通常用陣列或元組型別),若沒有特殊註記的話,通常會用 any 作表示 ## 拾、特殊型別 - Unknown Type ![](https://i.imgur.com/ECHf28n.png) > unknown 相對 any 來說,是一種更安全的型別機制 ### 1. unknown 的可以與不可以 - 只要當變數被註記為 any 或 unknown,該變數照樣都可以接收任意型別的值。 ![](https://i.imgur.com/JdYuDx8.png) - unknown 型別的值不能被強行指派到除了 any 或 unknown 型別外的任意型別變數 ![](https://i.imgur.com/8ulSEf7.jpg) ![](https://i.imgur.com/HMQTfpW.png) ![](https://i.imgur.com/IAQke4d.png) - 1. 只要程式根據判斷式與敘述式的結構,縮小變數在型別推論上的範疇,我們就可以讓純 unknown 型別的變數被指派到任意型別上 ![](https://i.imgur.com/P3R1rVY.png) - 2. 顯性的型別註記 ![](https://i.imgur.com/LzgwIT4.png) - 被標記為 unknown,不能呼叫任何方法或屬性,亦不可作為任何函式或方法之參數),any 型別不管亂呼叫什麼東西,都不會有事。 ![](https://i.imgur.com/JgaKxKX.png) ![](https://i.imgur.com/TOA0Iu1.png) - 若 unknown 變數被顯性地型別註記為某型別 T(其中 T 不為 unknown),則 unknown 變數可以作為該型別 T 之代表值,進行該型別底下合理之操作 ![](https://i.imgur.com/y1GlAoq.png) - 若 unknown 變數被控制流程限縮型別至某型別 T(其中 T 不為 unknown),則 unknown 變數可以作為該型別 T 之代表值,在該控制流程的範圍內進行合理之操作 ![](https://i.imgur.com/kdoM8N3.png) ### 2.寫一個安全的函式(或方法)把不安全的函式(或方法)包裝起來 把 JSON.parse 這種會回傳 any 的方法函式包裝起來。 ![](https://i.imgur.com/47vB9CK.png) ### 3. unknown 型別進行複合 type U = unknown & T => T type U = unknown | T => unknown type U = unknown | any => any ## 拾壹、型別化名 ![](https://i.imgur.com/0XPOvCI.png) 型別化名的意義就是把複雜格式(尤其是明文格式)的型別進行程式碼簡化與抽象化 ### 一、用法 若某型別 T, **T 可為任何的型別**: - **原始型別、 物件型別、 TypeScript 內建型別、 明文型別、 複合型別、 Generics 通用型別**等 其中我們想要讓該型別 T 等效於別名 A,則可以使用 TypeScript 的 type 關鍵字進行化名宣告: ``` type A = T ``` ![](https://i.imgur.com/U2zx3jt.png) ## 拾貳、介面宣告 X 使用介面 - TypeScript Interface Intro.. ![](https://i.imgur.com/aF3scLO.png) TypeScript Interface 可以藉由關鍵字 interface 宣告出來,介面裡面的詳細定義可為: - **物件格式**:即 JSON 格式,是為屬性對型別,不是對值 - **單一函式格式**:沒有任何屬性,就是函式而已,但不一定需要標上函式名稱 - **混合格式**:即『物件格式』與『單一函式格式』混合在一起 ![](https://i.imgur.com/OFxbQR6.png) ### 一、interface 檢查機制 - 多一鍵少一鍵都不行 ![](https://i.imgur.com/tJOwQw9.png) - 如果講物件帶入變數,再放進函式裡面,只要不少於 interface 定義的型別就不會出錯 ![](https://i.imgur.com/nKfF79P.png) - 為變數積極註記 ![](https://i.imgur.com/WyeQIzT.png) ### 二、interface 的介面擴展(Interface Extension / Inheritance) ![](https://i.imgur.com/pJr2DRL.png) - 多個介面要進行延伸,其中的兩個介面互不相容,就不能進行擴展的動作。 ![](https://i.imgur.com/ESPN3hi.png) ### 三、介面 Interface V.S. 型別 Type >- **介面(Interface)的意義** —— 跟規格的概念很像,可以擴充設計、組裝出更複雜的功能規格 > - **型別(Type)的意義** —— 代表靜態的資料型態,因此型別一但被定義出來則恆為固定的狀態。儘管可以利用型態的複合(intersection 與 union)看似達到型別擴展的感覺,然而這個行為並不叫作型別擴展,而是創造出新的靜態型別 - 介面使用上比較偏實作面,下面程式碼定義用戶帳號,而用戶帳號由"帳戶系統"和"帳戶個人資料"組成,藉由 interface 就可以把資料拆分的更細,讓管理程式變得輕鬆 ![](https://i.imgur.com/9yt5pVG.png) - interface 單字可以類比為 TypeScript 的介面,而 implementation 單字可以類比為型別系統裡的 type。 - 介面:規格(Spec)的概念,可以組裝、延展(使用 extends) - 型別:靜態的資料格式,不能被延展,每一次宣告新的型別化名 —— 對型別進行複合形式的操作 —— 都是在定義新的型別,不是延展作用 ## 拾参、函式超載 X 究極融合 - Function Overload & Interface Merging ![](https://i.imgur.com/ees1OnD.png)ㄋ功能多樣性 X 多樣性介面 - More on TypeScript Interface ### 一、函數超載 兼容不同情況函數輸入與輸出的型別註記 - 介面定義的屬性對應的函式可以- 進行超載的動作。 - 被超載的函式名稱必須相同。 - 若某物件實踐該介面時,必須符合該介面裡超載過的函式之所有情形 - 單純函式形式的介面也可以進行函式超載,差別在沒有名稱標記而已。 ![](https://i.imgur.com/GCeyRL1.png) ### 二、介面融合 - 若某介面 I 被重複定義多次,則該介面到最後的推論結果會是所有重複定義的介面的交集。 - I 若被重複定義時,裡面若干屬性跟過去所定義的某屬性相符的話,該屬性的型別必須跟過往定義出的介面裡的屬性之型別吻合。 ![](https://i.imgur.com/5x4S11V.png) - 整合第三方套件範例 ![](https://i.imgur.com/VgJbGnz.png) ## 拾肆、功能多樣性 X 多樣性介面 - More on TypeScript Interface ![](https://i.imgur.com/e0MpDGf.png) ### 一、Indexable Types - 模仿部分廣義物件的行為 indexable Types 在介面及型別都能使用 - 固定屬性的型別 - 對應 - 固定值的型別 ![](https://i.imgur.com/dWE9pv0.png) - Warning - 不能直接用陣列形式初始化值(除非是空陣列),由於陣列屬於 JS 物件的一種,而物件的屬性初始化時不允許為 string 以外的型態,因此初始化陣列的索引(index)都會以 '0', '1', '2' ... 這種字串的數字形式初始化,所以才會被 TypeScript 判定結果是錯誤! - 空陣列可以被初始化則是因為 TypeScript 認為都沒有屬性,沒有檢測之必要 - [index: number] 將索引部分鎖定在 number 型別上,目的是為了防止開發者呼叫字串型別的屬性(或是用點的方式呼叫屬性),而是模擬陣列的行為 -- 用數字來檢索該物件裡的內容 - 當物件必須在 [index: number] 這種狀態下初始化時,必須用 JSON 物件的格式,指定索引的位置(數值)並填入對應的值(當然,值必須符合型別 T,其中 T 為 [index: number]: T 裡面的 T) ``` [keyName: TKey]: TValue ``` - TKey 必須為 number 或者是 string 其中一種,不能為其他型別與 number 和 string 的複合格式(連 number | string 是不接受的!) - TValue 可為任意型別 ### 二、唯讀屬性 Readonly 在屬性前加入 readonly 的關鍵字,該屬性就會變成唯讀模式(Read-Only)。 ![](https://i.imgur.com/zluCbwf.png) ![](https://i.imgur.com/nhUw8Jn.png) ### 三、介面的混合格式 Hybrid Type Interface - 混合格式:即將『物件格式』跟『單一函式格式』混合在一起 ![](https://i.imgur.com/S96VdGQ.png) ![](https://i.imgur.com/jBm52xX.png) - 忘記實踐出混合型態介面裡的屬性與方法,TS 是不會檢查的 ## 拾伍、型別檢測 Type Guard - 控制流程分析 - 推論被限縮 ![](https://i.imgur.com/guBEngh.png) ![](https://i.imgur.com/hkkDCji.png) ### 一、行為限縮的技巧 - 若想要過濾出純原始型別的值的話,使用 typeof 操作子 - 若想要過濾出廣義物件型別的值的話,使用 instanceof 判斷操作子,並填上屬於該物件型別所屬的類別 - 其他方式,譬如 Array.isArray 可以檢測陣列