# Typescript 型別與介面
###### tags: `Typescript`
## 壹、前線維護・型別推論 X 註記 - Type Inference & Annotation
### 一、 基礎型別(Types)

- **原始型別 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)

#### 2.Nullable Types
- 但如果對 nothing 或 nothingLiterally 這兩個型別作確認的話,結果 TypeScript 會把它們推論成 any 型別,因為 null 或 undefined 被稱為 **Nullable Types**,被認為是 any 型別

- Nullable Types 會被認為是 any 型別,因此可以被指定為任何型態

#### 3. TDZ (Temporal Dead Zone,暫時性死區)
- 宣告一個變數後,在還沒指派值之前,用另外一個變數去接他,這時 TS 就會提出質疑。

- 在未確定變數被正式丟入合法的值之前的這段空間,不能使用該變數。

#### 4. 對遲滯性指派進行型別註記
```javascript=
let A: type;
A = B as type;
```
## 貳、物件型別 X 完整性理論 - Object Types Basics

### 一、基礎物件型別
#### 1. **JSON 物件格式**
TS 會提醒你推論後的型別是什麼

就算是 nullable 和 undefine 的型別也會出來,而不是 any

#### 2. 型別覆蓋的例子
```javascript=
let info = {
name: 'Maxwell',
age: 20,
hasPet: false,
}
let something = {
null: null,
define: undefined,
}
```
- **屬性值被錯誤的型別插入/覆寫干擾**
型別錯誤,TS 會主動給予提示

- **物件分別被正確或者是錯誤的物件格式整體複寫**
多一個鍵或少一個鍵都不行

- 型別推導時就會把整個物件,包括 key-value 的型別都作定義

- 主動註記物件型別時,會被標記為物件型別,因此只要是物件型別就可以付與值給它

- **直接對物件新增值**
新增或減少值會引起 TS 的警告

- 結論
- 對物件內的屬性覆寫,其值的型別必須相等
- 對整個物件覆寫,格式必須完全相等
- 用以下三種形式新增物件是允許的
```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. 將狹義物件用廣義物件覆蓋
實驗證實,在物件主動標記型別的狀態下,廣義定義的物件是可以覆蓋的

#### 2. 將廣義物件用狹義物件覆蓋

**廣義物件在被 TypeScript 推論的狀態下,屬性不能被任意新增或更改成其他型別**
能夠做的事情只有:
- 全面覆寫,廣義物件的屬性對照型別格式也要完全對位
- 更改廣義物件本身就擁有屬性對應的值,其中:要帶入的值的型態必須對應到該屬性的型態
## 參、函式型別 X 積極註記 - Function Types

### 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 會自動幫我們推導出輸出型別為何

也可以自行註記型別

- 如果輸出的型別為 any 該如何註記?

- 我們儘量不要讓變數被推導為 any 的型態。

- 函式不回傳值的狀態 void

- 驗證小實驗
- 只有定義回傳為 undefine 時,卻沒回傳值,TS檢查不會通過

## 肆、陣列型別 X 型別陣列 - Array Types

### 1.陣列型別 Array Types
- 單一陣列型別
TypeScript 會按照開發者認為的邏輯,把這種元素(Element)皆為同型別 T 的陣列用 T[] 的方式表示。

- 混合陣列型別

- 函式陣列型別

- 一個陣列中擁有不同種類的型別


- 陣列中物件擁有不一致數量的屬性

### 2. 定義變數的型別
如果要在陣列中加上一個 null,就得再定義陣列時使用 union 允許 null

## 肆、前線維護・陣列與函式 X 陣列與元組 - Array & Functions & Tuples

### 一、回呼函數
回呼函數一般情況下 TS 會幫你註記

### 二、陣列與元組 Arrays & Tuples
- 相對於型別註記(string|Date)[],我們只知道陣列中有這兩個型別,但不能指定哪個位置必須放哪個型別。
因此 TS 用元組來做註記

- 運用型別化名註記元組型別

- 元組與陣列表示法

- 不過元組的缺點就是:型別等同的兩個元素,儘管型別上是正確的,但是資料的意義上是不同的,就算被顛倒,TypeScript 也因為只會比對型別而不會進行警告

通常會建議資料儘量用 JSON 物件格式表示

## 伍、前線維護・列舉型別 X 主觀列舉 - Enumerated Types

### 一、列舉(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

只要是表達廣義物件的格式或者是任意型別(包含原始型別的)複合組合(union 與 intersection 也算在內就隸屬於明文型別的範疇。


### 一、型別化名 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 註記,則該變數被指派到的函式值,不需要積極註記,因為早在定義化名的時候,這個步驟就被做掉了 => 函式型別不用積極註記的第二種情況

- 廣義物件的積極註記
```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

### 一、 使用<prop>?選用屬性註記
當物件內的屬性,不是必須出現的狀況,可以使用 Optional Properties

- 若某屬性 P 屬於某物件的明文型別 的屬性之一,且該屬性對應的型別值為 T,而 A 是該明文型別的別名,則:
```
type A = {
P?: T
};
```
代表:
1. 選擇性地忽略 P 這個屬性。
2. 因為推論出來的結果會是以下的形式,因此也可以選擇寫出 P 屬性但填入 undefined 這個值:
```
{ P?: T | undefined }
```
### 二、複合類別

### 三、元組型別的選用元素

## 捌、特殊型別 - Never Type

- 無法跳脫出該函式或方法
- 出現例外結果中斷執行
- never 型別為所有型別的 subtype

## 玖、特殊型別 - Any
真的非不得已狀態下或者是快速測試下,可以使用 any。不過開發上,儘量不要用到 any 比較好;或者真不巧,遇到 any,也應當主動註記。
### 一、出現時機
- **遲滯性指派 Delayed Iniitialization**:變數定義時,除了未加註記(Type Annotation)外,也沒有指派值或者被指派為 Nullable Types。
- **一般宣告下的函式參數**:一般被宣告的函式,其參數通常會直接被推論為 any,又被稱作 Implicit any 的情形。此狀況是少數會被 TypeScript 主動通報的
- **函式回傳之值**:有些實務上,型別無法確定,因此到最後只能將回傳值預設為 any(如:JSON.parse)
- **未註記之空陣列**:沒有積極型別註記到的空陣列,其預設推論為 any[]
- **跟 I/O 行為有關**:例如,從外部 CSV 檔案讀取表格行格式(通常用陣列或元組型別),若沒有特殊註記的話,通常會用 any 作表示
## 拾、特殊型別 - Unknown Type

> unknown 相對 any 來說,是一種更安全的型別機制
### 1. unknown 的可以與不可以
- 只要當變數被註記為 any 或 unknown,該變數照樣都可以接收任意型別的值。

- unknown 型別的值不能被強行指派到除了 any 或 unknown 型別外的任意型別變數



- 1. 只要程式根據判斷式與敘述式的結構,縮小變數在型別推論上的範疇,我們就可以讓純 unknown 型別的變數被指派到任意型別上

- 2. 顯性的型別註記

- 被標記為 unknown,不能呼叫任何方法或屬性,亦不可作為任何函式或方法之參數),any 型別不管亂呼叫什麼東西,都不會有事。


- 若 unknown 變數被顯性地型別註記為某型別 T(其中 T 不為 unknown),則 unknown 變數可以作為該型別 T 之代表值,進行該型別底下合理之操作

- 若 unknown 變數被控制流程限縮型別至某型別 T(其中 T 不為 unknown),則 unknown 變數可以作為該型別 T 之代表值,在該控制流程的範圍內進行合理之操作

### 2.寫一個安全的函式(或方法)把不安全的函式(或方法)包裝起來
把 JSON.parse 這種會回傳 any 的方法函式包裝起來。

### 3. unknown 型別進行複合
type U = unknown & T
=> T
type U = unknown | T
=> unknown
type U = unknown | any
=> any
## 拾壹、型別化名

型別化名的意義就是把複雜格式(尤其是明文格式)的型別進行程式碼簡化與抽象化
### 一、用法
若某型別 T,
**T 可為任何的型別**:
- **原始型別、
物件型別、
TypeScript 內建型別、
明文型別、
複合型別、
Generics 通用型別**等
其中我們想要讓該型別 T 等效於別名 A,則可以使用 TypeScript 的 type 關鍵字進行化名宣告:
```
type A = T
```

## 拾貳、介面宣告 X 使用介面 - TypeScript Interface Intro..

TypeScript Interface 可以藉由關鍵字 interface 宣告出來,介面裡面的詳細定義可為:
- **物件格式**:即 JSON 格式,是為屬性對型別,不是對值
- **單一函式格式**:沒有任何屬性,就是函式而已,但不一定需要標上函式名稱
- **混合格式**:即『物件格式』與『單一函式格式』混合在一起

### 一、interface 檢查機制
- 多一鍵少一鍵都不行

- 如果講物件帶入變數,再放進函式裡面,只要不少於 interface 定義的型別就不會出錯

- 為變數積極註記

### 二、interface 的介面擴展(Interface Extension / Inheritance)

- 多個介面要進行延伸,其中的兩個介面互不相容,就不能進行擴展的動作。

### 三、介面 Interface V.S. 型別 Type
>- **介面(Interface)的意義** —— 跟規格的概念很像,可以擴充設計、組裝出更複雜的功能規格
> - **型別(Type)的意義** —— 代表靜態的資料型態,因此型別一但被定義出來則恆為固定的狀態。儘管可以利用型態的複合(intersection 與 union)看似達到型別擴展的感覺,然而這個行為並不叫作型別擴展,而是創造出新的靜態型別
- 介面使用上比較偏實作面,下面程式碼定義用戶帳號,而用戶帳號由"帳戶系統"和"帳戶個人資料"組成,藉由 interface 就可以把資料拆分的更細,讓管理程式變得輕鬆

- interface 單字可以類比為 TypeScript 的介面,而 implementation 單字可以類比為型別系統裡的 type。
- 介面:規格(Spec)的概念,可以組裝、延展(使用 extends)
- 型別:靜態的資料格式,不能被延展,每一次宣告新的型別化名 —— 對型別進行複合形式的操作 —— 都是在定義新的型別,不是延展作用
## 拾参、函式超載 X 究極融合 - Function Overload & Interface Merging
ㄋ功能多樣性 X 多樣性介面 - More on TypeScript Interface
### 一、函數超載
兼容不同情況函數輸入與輸出的型別註記
- 介面定義的屬性對應的函式可以- 進行超載的動作。
- 被超載的函式名稱必須相同。
- 若某物件實踐該介面時,必須符合該介面裡超載過的函式之所有情形
- 單純函式形式的介面也可以進行函式超載,差別在沒有名稱標記而已。

### 二、介面融合
- 若某介面 I 被重複定義多次,則該介面到最後的推論結果會是所有重複定義的介面的交集。
- I 若被重複定義時,裡面若干屬性跟過去所定義的某屬性相符的話,該屬性的型別必須跟過往定義出的介面裡的屬性之型別吻合。

- 整合第三方套件範例

## 拾肆、功能多樣性 X 多樣性介面 - More on TypeScript Interface

### 一、Indexable Types - 模仿部分廣義物件的行為
indexable Types 在介面及型別都能使用
- 固定屬性的型別 - 對應 - 固定值的型別

- 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)。


### 三、介面的混合格式 Hybrid Type Interface
- 混合格式:即將『物件格式』跟『單一函式格式』混合在一起


- 忘記實踐出混合型態介面裡的屬性與方法,TS 是不會檢查的
## 拾伍、型別檢測 Type Guard
- 控制流程分析
- 推論被限縮


### 一、行為限縮的技巧
- 若想要過濾出純原始型別的值的話,使用 typeof 操作子
- 若想要過濾出廣義物件型別的值的話,使用 instanceof 判斷操作子,並填上屬於該物件型別所屬的類別
- 其他方式,譬如 Array.isArray 可以檢測陣列