---
# System prepended metadata

title: TypeScript 學習筆記

---

# TypeScript 學習筆記

## Getting Started

### 為什麼要用 TypeScript

TypeScript的規範較為嚴謹，可以藉由定義型別來避開原本可能要打更多程式碼才能處理的錯誤。

### TypeScript 加了什麼料

* 和它的名字一樣, Types !
* 能藉由二次編譯來在老舊的瀏覽器上使用更新的JavaScript功能
* JavaScript沒有的一些酷東西
* 像是Decorator的元程式設計(Meta-Programming)功能
* 擁有非常高的自訂性

### 課程 Roadmap

1. Getting Started
    * TypeScript的初步介紹
2. TypeScript Basic
    * TypeScript 基礎篇
3. Compiler & Configuration Deep Dive
    * TypeScript 編譯器詳細設定介紹
4. Working with Next-gen JS Code
    * 如何在 TypeScript 中使用新潮的 JavaScript 功能
5. Classes & Interfaces
    * TypeScript 的類別與介面
6. Advanced Types & TypeScript Features
    * 進階的功能與特殊型別
7. Generics
    * 認識 Generics
8. Decorators
    * 認識 Decorators
9. Time to Practice - Full Project
    * 見證奇蹟的時刻 - 製作一個完整專案
10. Working with Namespace & module
    * 學習使用 Namespace 和 module 來改善專案設計
11. Webpack & TypeScript
    * 讓 TypeScript 與 Webpack 共舞
12. Third-Party Libraries & TypeScript
    * 第三方函式庫的崛起
13. React+TypeScript & Nodejs+TypeScript
    * React 框架 x TypeScript x Nodejs

---

## TypeScript Basic

:::info
:book: **TypeScript 基礎篇**
:::

### Core Types

:::success
:sparkles: 主要常見型別
:::
* JavaScript 原生延伸
  * **number 數字**
    * Ex: 1, 5.3, -10
    * 任何的數字都屬於這個型別
  * **string 字串**
    * Ex: "Hello", 'Hello', \`Hello\`
    * 所有的文字陣列(字串)
  * **boolean 布林函數**
    * Ex: true, false
    * 只包含 true, false  兩種值
  * **object 物件**
    * Ex: { age: 30 }
    * 等同於 JavaScript 中的物件
  * **Array 陣列**
    * Ex: ["hello", "world"]
    * 等同於 JavaScript 中的陣列
* TypeScript 新型別
  * **Tuple 元組**
    * Ex: [1, "hello"]
    * 限制存取型別及長度的陣列
    * 定義初值方式同陣列，型別定義方式則不一樣
  * **Union 聯合型別**
    * ```typescript=
        // example
        function example( a: number | string, b: number | string ) {
          // ...function content
        }
      ```  
    * 可以一次代表多個型別
    * 多用於函式的參數預定義
  * **Literal 字面值型別**
    * ```typescript=
        // example
        let example: "hello" ;
        let example_literal_union: "hello" | "world"
      ```
    * 變數值將只能指派預先已經設定好的值
    * 接受三種類別作為其成員: `string`, `number`, `boolean`
    * 使用場景類似 `switch...case`
    * 通常會和 Union 一起使用
  * **enum 列舉**
    * ```typescript=
        // example
        enum Role{ ADMIN, READ_ONLY, AUTHOR, CUSTOM = 100 }
      ```
    * 將數組轉換成易讀的文字使用
  * **unknown 未知型別**
    * ```typescript
      // example
      let unknownUserInput: unknown
      let anyUserInput: any
      let userName: string
      
      // 假設使用者輸入的是一般字串
      userName = unknownUserInput // 會出現錯誤
      userName = anyUserInput // 不會出現錯誤
      
      // 前者需要經過一層型別檢查才能賦值給其他型別的變數
      if ( typeof unknownUserInput === "string" ) {
        userName = unknownUserInput
      }
      ```
    * unknown 設計上雖然比 any 來的好，但仍然不是個該被過分使用的型別。
  * 
  * **any 軟型別**
    * 任何型態的預設值皆屬於這個型別
:::warning
:warning: **注意!!** :warning:

部分預設型別的英文為小寫，例如`string`或`number`，這是 TypeScript 自身追加的規範。
為的是和 JS 原生的`String`和`Number`做區分，兩者在函式庫上的支援亦有些許差異。
原則上 JS 屬於 ***弱型別語言***，故在撰寫 TS 的時候，***不應該*** 繼續沿用 JS 原生的型別，因為原生型別並沒有強制性，而只是一種 ***標註***。
:::

### TypeScript 預定義型別方法

#### 一般定義
```typescript=
const num_1: number ; // 在變數後打上冒號並追加想要的型別
const str_1: string ;

// 通常只會使用在未定義初值，只定義型態的時候
// 因為 tsc 編譯時可以自動抓取你定義的初值的型態，所以不需要手動再給一次型別
```

#### 函式參數
```typescript=
// 在參數後打上冒號並追加想要的型別
// 在參數後加上問號代表該參數非必要
function add( num1: number, num2: number, num3?: number ) { 
  if ( num3 ) {
    return num1 + num2 + num3
  }
  return num1 + num2 ;
}

const num_1 = 5;
const num_2 = 2.8;

const result = add( num_1, num_2 );
console.log( result )

// 當參數需要支援多種型別時，則使用 Union
function combine( left: number | string, right: number | string ) {
  // 利用 if...else 限縮編譯器接收到的實際型別
  if ( typeof left === "number" && typeof right === "number" ) {
    return left + right;
  }
  else {
    return left.toString() + right.toString()
  }
}
```

#### 陣列型別定義
```typescript=
let myHobbies: string[];
myHobbies = ["Sports"]

// tsc 在處理時，會自動將所有成員的型別認定為您預定義的型別
// 可以保證你在使用相關內建函式時不會出現錯誤
```

#### 物件成員定義
```typescript=
const person: {
  name: string;
  age: number;
}

person = {
  name: "YumekuiiNight",
  age: 30
}

/**
 * 若預定義的方式同前面一樣只用 :object 的話，
 * 會出現 tsc 編譯時無法知道內部成員的情況，
 * 甚至有可能造成額外的錯誤
 */
 
// 以下則是巢狀物件的定義方法
const _nested_Product: {
  id: string;
  price: number;
  tags: string[];
  details: {
    title: string;
    description: string;
  }
}

_nested_Product = {
  id: 'abc1',
  price: 12.99,
  tags: ['great-offer', 'hot-and-new'],
  details: {
    title: 'Red Carpet',
    description: 'A great carpet - almost brand-new!'
  }
}
```

#### Tuple 元組定義
```typescript=
const myTuple: [number, string];
myTuple = [2, "hello"]

/**
 * 因為 Tuple 在編譯後仍然是用 Array 去實現的，
 * 所以少部分內建函式在不應該能運作的時候仍然是可以運作的
 */
myTuple.push("world") // 不會出現錯誤
myTuple = [0, "init", "something else"] // 會出現錯誤
```

#### 自定義型別
```typescript=
// 類似 C 的 struct
// 將 Union, Literal 或 object 型別的變數再做一次包裝
type combinable = number | string ;
type conversionDescriptor = "as-text" | "as-number"
type User = { name: string, age: number }

// 也可以包裝一般的型別
type helloNumber = 0
type helloString = "HelloWorld"
type helloBoolean = true

// 使用方法
let myNumber: helloNumber = 0 // 若給 0 以外的值會出現錯誤
    
function combine( 
  left: combinable,
  right: conbinable,
  resultCase: conversionDescriptor,
  user: User 
) {
  // ...function content
}
```

### TypeScript 函式

#### 原生 JavaScript 寫法

```javascript=
function example( a, b ) {
  // content...
  return result
}
```

* 參數不需要定義型別
* 函式本身沒有回傳型別

#### TypeScript 寫法

```typescript=
function example( a: number, b: number ): number {
  // content...
  return result
}
```

#### 不強制回傳(Void)

```typescript=
function example( param: string ): void {
  // content
  // no return
}
```
* 定義上類似但有別於 JavaScript 的 undefined，兩者不可通用

#### 不回傳(Never)

```typescript=
function example( param: string ): never {
  // content
  // should not have return
}
```

#### 函式(Function)也是一種類別

```typescript=
// 代表任何函式
let listener_a: Function;

// 代表需要兩個參數，並且回傳型別為 number 之函式
let listener_b: ( a: number, b: number ) => number;

// 參數的名稱並不會造成影響，參數的個數可以向下兼容
listener_b = param => {
  // 內容...
}
```

---

## Compiler & Configuration Deep Dive

:::info
:book: **TypeScript 編譯器詳細設定介紹**
:::

### Initialization 專案初始化
* ```powershell=
  tsc --init
  ```
* 這個指令將會把現在所在的資料夾視為一個 typescript 的專案資料夾
* 會加入一個新檔案，名叫 `tsconfig.json`，包含所有關於 typescript 編譯器的詳細設定

### Watch Mode 監看模式

* 功能和 nodemon 相同，不同的是它內建於 typescript 的編譯器中
* ```powershell=
  tsc <filename> --watch
  // or
  tsc <filename> -w
  
  // 有先進行 Initialize 的資料夾，可以直接監看該資料夾下的所有檔案變化
  tsc --watch
  // or
  tsc -w
  ```
* typescript 將會監視該檔案的所有變化，當有變化的時候便重新編譯。

### Exclude & Include 特定檔案的排除與加入

* ```json=
  // tsconfig.json
  {
    compilerOptions: {
      // ...
      // ..
      // .
    },
    "exclude": [],
    "include": [],
    
    // 單一檔案導入
    "files": []
  }
  ```
* node_modules 在沒有加入 exclude 設定時，預設會被自動排除
* 當 tsconfig.json 存在有 exclude / include 這兩種屬性時，將會只排除 / 加入列表中的檔案
* include 會優先於 exclude ( include - exclude = 真正被加入的檔案 )

### CompilerOptions 介紹

:::spoiler 完整設定文件
```json=
  // tsconfig.json
  {
    "compilerOptions": {

      /* Projects */
      // "incremental": true,
      // "composite": true,
      // "tsBuildInfoFile": "./.tsbuildinfo",
      // "disableSolutionSearching": true,
      // "disableReferencedProjectLoad": true,

      /* Language and Environment */
      "target": "es2016",                                  
      // "lib": [],                                        
      // "jsx": "preserve",                                
      // "experimentalDecorators": true,                   
      // "emitDecoratorMetadata": true,                    
      // "jsxFactory": "",                                 
      // "jsxFragmentFactory": "",                         
      // "jsxImportSource": "",                            
      // "reactNamespace": "",                             
      // "noLib": true,                                    
      // "useDefineForClassFields": true,                  
      // "moduleDetection": "auto",                        

      /* Modules */
      "module": "commonjs",                                
      // "rootDir": "./",                                  
      // "moduleResolution": "node",                       
      // "baseUrl": "./",                                  
      // "paths": {},                                      
      // "rootDirs": [],                                   
      // "typeRoots": [],                                  
      // "types": [],                                      
      // "allowUmdGlobalAccess": true,                     
      // "moduleSuffixes": [],                             
      // "resolveJsonModule": true,                        
      // "noResolve": true,                                

      /* JavaScript Support */
      // "allowJs": true,                                  
      // "checkJs": true,                                  
      // "maxNodeModuleJsDepth": 1,                        

      /* Emit */
      // "declaration": true,                              
      // "declarationMap": true,                           
      // "emitDeclarationOnly": true,                      
      // "sourceMap": true,                                
      // "outFile": "./",                                  
      // "outDir": "./",                                   
      // "removeComments": true,                           
      // "noEmit": true,                                   
      // "importHelpers": true,                            
      // "importsNotUsedAsValues": "remove",               
      // "downlevelIteration": true,                       
      // "sourceRoot": "",                                 
      // "mapRoot": "",                                    
      // "inlineSourceMap": true,                          
      // "inlineSources": true,                            
      // "emitBOM": true,                                  
      // "newLine": "crlf",                                
      // "stripInternal": true,                            
      // "noEmitHelpers": true,                            
      // "noEmitOnError": true,                            
      // "preserveConstEnums": true,                       
      // "declarationDir": "./",                           
      // "preserveValueImports": true,                     

      /* Interop Constraints */
      // "isolatedModules": true,                          
      // "allowSyntheticDefaultImports": true,            
      "esModuleInterop": true,                           
      // "preserveSymlinks": true,                        
      "forceConsistentCasingInFileNames": true,            

      /* Type Checking */
      "strict": true,                                      
      // "noImplicitAny": true,                           
      // "strictNullChecks": true,                         
      // "strictFunctionTypes": true,                     
      // "strictBindCallApply": true,                      
      // "strictPropertyInitialization": true,             
      // "noImplicitThis": true,                           
      // "useUnknownInCatchVariables": true,               
      // "alwaysStrict": true,                             
      // "noUnusedLocals": true,                          
      // "noUnusedParameters": true,                      
      // "exactOptionalPropertyTypes": true,              
      // "noImplicitReturns": true,                       
      // "noFallthroughCasesInSwitch": true,              
      // "noUncheckedIndexedAccess": true,                
      // "noImplicitOverride": true,                       
      // "noPropertyAccessFromIndexSignature": true,      
      // "allowUnusedLabels": true,                        
      // "allowUnreachableCode": true,                     

      /* Completeness */
      // "skipDefaultLibCheck": true,                     
      "skipLibCheck": true                                 
    }
  }
```
:::

:::spoiler 更新紀錄
  `2022/8/1 目前只會註記常用的設定的解釋，以後有機會會陸續補上。`
:::

* **target**
  * 預設: "es2016" ( 預設會隨著時間改變，這是 2022/8/1 進行 Init 後得出的結果 )
  * 規定將 TypeScript 編譯成 JavaScript 時，要遵照哪一個版本的 ECMAScript 規範

* **lib**
  * 預設: 會根據 target 的值而有所不同
  * 允許 TypeScript 使用某些環境下自帶的函式庫，如: Dom 的 `document`

* **allowJS**
  * 允許該專案的資料夾內存在有 `*.js` 結尾的檔案

* **checkJS**
  * 設定是否回報 `*.js` 中的潛在錯誤

* **jsx**
  * 決定 jsx / tsx 內部的 HTMLElement 的定義方法

* **sourceMap**
  * 允許瀏覽器可以讀取並對 TypecScript 文件進行除錯
  * 會為每一個 TypeScript 檔案生成一個專門供瀏覽器讀取的 `*.js.map` 檔

* **outDir**
  * 設定 TypeScript 編譯完後的 `*.js` 檔案的存放位置

* **rootDir**
  * 設定該專案的根資料夾，設定的資料夾需要包含專案資料夾內的所有 `*.ts` 檔
  * rootDir 的設定不會影響 include / exclude 的相對路徑結果

* **removeComments**
  * 在 TypeScript 編譯完成後去除所有註解

* **noEmit**
  * 禁止所有編譯行為

* **noEmitOnError**
  * 當專案出現錯誤時強制禁止編譯行為

* **downlevelIteration**
  * 允許使用舊版本的 ECMAScript 來實作高版本的的特殊迴圈，來達到更精確的效果
  * 通常使用在 target 版本較舊時

* **strict**
  * 告訴編譯器將要使用 strict mode 來編譯出 `*.js`
  * 效果等同於將 strict mode 相關的所有 options 設為 true

  * **noImplicitAny**
    * 禁止隱式宣告 `any` 型別
    * ```typescript=
      // 隱式宣告 param 為 any 型別
      function example( param ) { // 會在這行出現錯誤
        console.log( param )
      }
      
      example("Hello World!")
      ```
  * **strictNullChecks**
    * 強迫開發者檢查變數型別是否為 `undefined` 或 `null`
    * 以防呼叫相關函式的失敗
    * ```typescript=
      const button = document.querySelector("button") // 會出現錯誤
      
      // 可以在宣告式結尾加上一個驚嘆號
      // 代表略過 null check
      const div = document.querySelector("div")! 
      
      // 以下是比較嚴謹的作法
      if ( button ) {
        button.addEventListener("click", click_handler)
      }
      ```
      
  * **strictBindCallApply**
    * 檢查 'bind', 'call', 'apply' 所使用的參數是否和原函式相符

  * **alwaysStrict**
    * 在編譯出的 `*.js` 開頭加上 `use strict` 來保證編譯出的檔案正在使用 strict mode

* **noUnusedLocals**
  * 在有區域變數未被使用時發出警告

* **noUnusedParameters**
  * 在有函式參數未被使用時發出警告

* **noImplicitReturns**
  * 禁止在任何情況下，在函式中省略 return 關鍵字
  * 若該函式不一定需要回傳，則仍必須在函式結束點加上 return
  * ```typescript=
    function example( value ) {
      if ( value === 0 ) {
        return 1
      }
      
      // 需要另外加上 return 關鍵字
      // 注意函式結束點不一定是函式結尾
      return
    }
    ```

* allowUnusedLabels
  * 允許未使用的標籤的存在
  * ```typescript=
    function verifyAge(age: number) {
      if (age > 18) {
        verified: true;
      }
    }
    ```
    
* allowUnreachableCode
  * 允許語法上導致無法執行的程式區段的存在
  * type guard 造成的無法執行區段則不受此規則影響

* 

---

## Working with Next-gen JS Code

:::info
:book: **如何在 TypeScript 中使用新潮的 JavaScript 功能**
:::

:::info
:fast_forward: 本章節主要為 ES6 相關，與 TypeScript 本身相關度較低，故暫時跳過
:::

## Classes & Interfaces

:::info
:book: **TypeScript 的類別與介面**
:::

### First Class

```typescript=
class Department {
  /* 成員變數宣告 */
  name: string
  
  /* 類別建構子 */
  constructor( n: string ) {
    this.name = n
  }
  
  /* 成員函式 */
  describe( this: Department ) {
    console.log( "Department" + this.name )
  }
}
```

### 根據 class 建立新物件
```typescript=
// after Department class defined

// 當呼叫 new 關鍵字時，TypeScript 會自動找到 class 的 constructor 執行
const accounting = new Department( "Accounting" );
```

### 在成員函式的參數中加上 this 關鍵字？

```typescript=
// after Department class defined
const accounting = new Department( "Accounting" );

accounting.describe(); // 預期輸出: Department: Accounting

const accountingCopy = { describe: accounting.describe }

/** 
 *  TypeScript 會因為參數中的 this 關鍵字的原因
 *  知道這個函式會使用 this 的某個 property
 *  而因為 accountingCopy.describe() 指向的 this
 *  會被重新定義到 accountingCopy 本身
 *  故 TypeScript 這時找不到 this.name
 *  便會在這邊報錯
 */
accountingCopy.describe(); // 會出現錯誤
```

### 公用與私用 ( Public & Private )

```typescript=
// rewrite Department
class Department { 
  // 預設若不帶 public / private 關鍵字時，成員都會是 public 公用的
  public name: string // 公用變數
  private employees: string[] = [] // 私用變數
  
  constructor( n: string ) {
    this.name = n
  }
  
  describe( this: Department ) {
    console.log( "Department" + this.name )
  }

  addEmployee( employee: string ) {
    this.employees.push( employee )
  }

  printEmployeeInformation() {
    console.log( this.employees.length )
    console.log( this.employees )
  }
}

const accounting = new Department( "Accounting" )

/**
 * 私用成員外部不可存取
 * 雖然是這樣說，但是 TypeScript 是基於 JavaScript 設計的語言
 * 而 ECMAScript 一直到最近才有所謂公用私用的概念出現
 * 所以這樣的 code，雖然一定會在 TypeScript 編譯器中報錯
 * 但是如果意外編譯成功，編譯出的 JavaScript 還是有可能可以這樣執行的
 */
accounting.employees[ 2 ] = "Anna" // 會出現錯誤

// 公用成員外部可以存取
accounting.name = "NEW NAME"

// 私用成員應該透過內部函式做存取或修改
accounting.addEmployee( "John" )
accounting.addEmployee( "Kao" )

accounting.printEmployeeInformation()
// 預期輸出
// 2
// ["John", "Kao"]
```

### Shorthand Contructor
```typescript=
// rewrite Department
class Department {
  
  // private id: string
  // private name: string
  
  // 可以直接在 constructor 定義成員變數
  constructor( private id: string, private name: string ) {
    // this.id = id
    // this.name = n
  }

  describe( this: Department ) {
    console.log( `Department (${ this.id }): this.name` )
  }
}

const accounting = new Department( "b1", "Accounting" )
accounting.describe()
// 預期輸出
// Department (b1): Accounting 
```

### 唯讀成員變數 ( Readonly ) 

```typescript=
// rewrite Department
class Department {
  
  // private readonly id: string
  // private name: string
  
  // readonly 關鍵字只能用在 class 或 interface 中
  // 用途和 const 類似，都是禁止 reassign 的動作
  constructor( private readonly id: string, private name: string ) {
    // this.id = id
    // this.name = n
  }

  describe( this: Department ) {
    console.log( `Department (${ this.id }): this.name` )
  }
}
```

### 繼承 ( Inheritance )
```typescript=
// rewrite Department
class Department {
  
  // private readonly id: string
  // private name: string

  constructor( private readonly id: string, private name: string ) {
    // this.id = id
    // this.name = n
  }

  describe( this: Department ) {
    console.log( `Department (${ this.id }): this.name` )
  }
}

class ITDepartment extends Department {
  admins: string[]
  
  // 子類別在沒有寫任何內容的情況下
  // 預設會複製所有父類別的可繼承的成員變數或成員函式
  
  constructor( id: string, admins: string[] ) {
    // super 關鍵字只能用在有繼承父類別的類別
    // 會呼叫父類別的 this 作使用
    super( id, "IT" )
    
    // 在子類別中，如果需要呼叫 this 關鍵字，順序都必須在 super 關鍵字之後
    this.admins = admins
  }
}

const it = new ITDepartment( "d1", ["Max"] )

console.log( it )
/** 
 * 預期輸出
 * ITDepartment {
 *   id: 'd1',
 *   name: "IT",
 *   employees: [],
 *   admins: ["Max"]
 * }
```

### Override & protected

```typescript=
// rewrite Department & ITDepartment
class Department {
  
  private readonly id: string
  private name: string
  
  // 若希望父類別傳遞成員給子類別，但又不希望使用 public 導致外部也能存取時
  // 則使用 protected
  protected employees: string[]

  constructor( id: string, name: string ) {
    this.id = id
    this.name = name
  }

  describe( this: Department ) {
    console.log( `Department (${ this.id }): ${ this.name }` )
  }

  addEmployees( employee: string ) {
    this.employees.push("John")
  }
}

class ITDepartment extends Department {
  admins: string[]
  
  constructor( id: string, admins: string[] ) {
    super( id, "IT" )    
    this.admins = admins
  }

  // 這邊的 addEmployees 會覆寫父類別的 addEmployees
  addEmployees( employee: string ) {
    if ( !this.admins.includes( employee ) ) {
      this.employees.push( employee )
    }
  }
}
```

#### Public vs Protected vs Private

|        | **Public** | **Protected** | **Private** |
|:------:|:----------:|:-----------:|:-------------:|
|  內部   |    可存取    |     可存取      |      可存取      |
|  外部   |    可存取    |    不可存取     |     不可存取      |
| 子類別  |    可存取    |     可存取      |     不可存取      |

### 抽象類別 ( Abstract class )

```typescript=
// rewrite Department

// 抽象類別需要在 class 前面加上關鍵字 abstract
// 另外抽象類別只能被其他類別繼承，不得使用該類別進行實例化
abstract class Department {
  private readonly id: string
  private name: string

  constructor( id: string, name: string ) {
    this.id = id
    this.name = name
  }

  /**
   * 這代表強制任何繼承該類別的子類別
   * 必須要實作出 describe 函式
   * abstract 關鍵字也可以加在變數前
   */ 
  abstract describe( this: Department ): void;
}

class ITDepartment extends Department {
  describe() {
    console.log( "${ this.name } Department - ID: ${ this.id }" )
  }
}

const it = new ITDepartment( "d1", "IT" )
it.describe() // 預期輸出: IT Department - ID: d1 
```

### Getters and Setters

```typescript=
// rewrite Department
class Department {
  
  private readonly id: string
  private name: string
  private lastEmployee
  
  // 若希望父類別傳遞成員給子類別，但又不希望使用 public 導致外部也能存取時
  // 則使用 protected
  protected employees: string[]

  constructor( id: string, name: string ) {
    this.id = id
    this.name = name
  }

  get latestEmployee() {
    if ( this.lastEmployee ) {
      return this.lastEmployee
    }
    
    throw new Error("No employee found.")
  }

  set latestEMployee( employee ) {
    if ( !employee ) {
      throw new Error("Please enter a valid value.") 
    }
    
    this.addEmployee( employee )
  }

  describe( this: Department ) {
    console.log( `Department (${ this.id }): ${ this.name }` )
  }

  addEmployees( employee: string ) {
    this.employees.push("John")
    this.lastEmployee = employee
  }
}

coonst accounting = new Department( "b1", "Accounting" )

accounting.addEmployess("Max")
accounting.addEmployess("John")

// getter 和 setter 都是在發生相關動作時會被呼叫
// 所以在 access 的時候，不需要像函式一樣加上括號
console.log( accounting.latestEmployee ) // 預期輸出: ["Max", "John"]
accounting.lastestEmployee = "Jack" // 預期輸出: ["Max", "John", "Jack"]
```

### 靜態屬性與靜態方法 ( static )

```typescript=
// rewrite Department
class Department {
  
  private readonly id: string
  private name: string
  private employees: string[] = []
  
  // 加上 static 後的變數或函式，可以不需要實例化，直接透過 class 來 access
  static fiscalYear: number = 2022

  constructor( id: string, name: string ) {
    this.id = id
    this.name = name
  }

  static createEmployee( name: string ) {
    return { "name": name }
  }

  describe( this: Department ) {
    console.log( `Department (${ this.id }): this.name` )
  }

  addEmployee( employee: { name: string } ) {
    this.employees.push( employee )
  }
}

const accounting = new Department( "b1", "Accounting" )
const newEmployee = Department.createEmployee( "Jack" )
accounting.addEmployee( newEmployee ) // [ { name: "Jack" } ]
console.log( Department.fiscalYear ) // 預期輸出: 2022
```

### 私用建構子與單例設計模式 ( Singleton & Private constructor )

```typescript=
// rewrite Department

/**
 * 建構子可以被設定為 private
 * 多半使用在一種被稱為單例設計模式的寫法
 * 藉由將建構子私有化，並且將產生出的 instance 存在 class 內
 * 再透過 static 方法將 instance 傳出去
 * 來保證一個類別必定只會被實作一次
 */

class Department {
  
  private readonly id: string
  private name: string
  private static instance: Department

  private constructor( id: string, name: string ) {
    this.id = id
    this.name = name
  }

  static getInstance() {
    if ( this.instance ) {
      return this.instance
    }
    this.instance = new Department( "b1", "Accounting" )
    return this.instance
  }
}

const account = Department.getInstance()
```

---

### First Interface

```typescript=
/** 
 * interface 中不可包含變數值的宣告
 * 亦不可包含函式的實作
 * 形式上比較接近 type
 * 但 interface 只能包裝 object
 */
interface Person {
  name: string;
  age: number;
  
  greet( pharse: string ): void;
}

// 一般使用方法
// 幾乎和 type 沒有差別
let user_1: Person;

user_1 = {
  name: 'Max',
  age: 30,
  greet( pharse: string ) {
    console.log( pharse + ' ' + this.name )
  }
}

user_1.greet( "Hello, I am " )
```

### Interfaces with classes

```typescript=
interface Greetable {
  // interface 中無法使用 public / protected / private
  // 但可以使用 readonly
  readonly name: string;
  
  // interface 中也可以存在有非必要參數
  // 非必要參數被繼承到 class 時，則不一定需要被宣告或實作
  
  greet( pharse: string ): void;
}

/**
 * 和 abstract class 類似
 * class 可以以 interface 為基底來實作
 * 一樣會強制該 class 需要實作或宣告 interface 中所有成員
 * 另外和 abstract class 不同的是，一個 class 可以同時實作多個 interface
 */

class Person implements Greetable {
  // 父介面定義為 readonly 的屬性
  // 將會在 class 中繼續保留唯獨的特性
  // 但可以不用再添加一次關鍵字
  name: string;
  
  // 可以根據需要自行增加變數或函式
  age: number = 30
  
  constructor( n: string ) {
    this.name = n
  }
  
  greet( pharse: string ) {
    console.log( pharse + ' ' + this.name )
  }
}

// 使用 interface 作為型別的變數
// 可以存放實作該 interface 的 class 的實例
let user_1: Greetable

user_1 = new Person( "Max" )

user_1.greet( "Hello, I am " )
```

### Interface 的繼承

```typescript=
// 您可以選擇用 interface 互相繼承的方法
// 也可以選擇讓單一 class 實作多個 interface 
// 兩這差異不大
interface Named {
  readonly name: string
}

interface Greetable extends Named {
  greet( pharse: string ): void;
}

class Person implememts Greetable {
  name: string
  age: number = 30
  
  greet( pharse: string ) {
    console.log( pharse + ' ' + this.name )
  }
}
```

### Interface as Function Type

```typescript=
// 需要有兩個 number 的變數，並回傳 number 型別
interface mathFunction {
  ( a: number, b: number ) => number;
}

let add: mathFunction

add = ( left: number, right: number ) => {
  return left + right;
}
```



## Advanced Types & TypeScript Features

:::info
:book: **進階的功能與特殊型別**
:::

### Intersection Types
```typescript=
type Admin = {
  name: string;
  privileges: string[];
}

type Employee = {
  name: string;
  startDate: Date;
}

type ElevatedEmployee = Admin & Employee

// 等同於以下
// interface ElevatedEmployee extends Admin, Employee
// 為兩種 object 的增集
// 但 type 的寫法較為簡潔

const el: ElevatedEmployee = {
  name: "Max",
  privileges: [ "createServer" ]
  startDate: new Date()
}

type Combinable = string | number
type Numeric = number | boolean


// typeof Universal === number
type Universal = Combinable & Numeric
```

* Object 的情況，尋找 OR 集合
* Union 的情況，尋找 AND 集合

### More about Type Guards
```typescript=
type Admin = {
  name: string;
  privileges: string[];
}

type Employee = {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin

function printEmployeeInformation( emp: UnknownEmployee ) {
  // 因為 Employee 和 Admin 都不是原生型別
  // 所以這裡無法用 typeof 來確認
  // 要利用是否有該成員變數來判斷
  
  if ( "privileges" in emp ) {
    console.log( "Privileges: " + emp.privileges )
  }
  if ( "startDate" in emp ) {
    console.log( "Join Date:  " + emp.startDate )
  }
}

class Car {
  drive() {
    console.log("Drining......")
  }
}

class Truck {
  drive() {
    console.log("Driving a truck......")
  }
  
  loadCargo( amount: number ) {
    console.log("Loading Cargo..." + amount)
  }
}

type Vehicle = Car | Truck

const v1 = new Car()
const v2 = new Truck()

function useVehicle( vehicle: Vehicle ) {
  vehicle.drive()
  
  // 透過    可以知道 vehicle 的型別
  // 背後雖然 JavaScript 不知道 Truck
  // 但可以透過比對 constructor 來知道
  if ( vehicle instanceof Truck ) {
    vehicle.loadCargo( 1000 )
  }
}

useVehicle( v1 )
useVehicle( v2 )
```
### Discriminated Union
```typescript=
// 藉由在 interface 中加入指定的 literal 型態變數
// 來判斷 interface 的型別
interface Bird {
  type: "bird",
  flyingSpeed: number
}

interface Horse {
  type: "horse",
  runningSpeed: number
}

type Animal = Bird | Horse

function moveAnimal( animal: Animal ) {
  let speed;
  switch( animal.type ) {
    case "bird":
      speed = animal.flyingSpeed
      break
    case "horse":
      speed = animal.runningSpeed
      break
  }
  consoel.log( 'Moving at speed: ' + speed )
}

moveAnimal({ type: "bird", flyingSpeed: 10 })
```

### Type Casting
**HTML**
```htmlembedded=
<input type="text" id="user-input">
```

**TypeScript**
```typescript=
let userInputElement = document.getElementById("user-input")

// 直接告知 TypeScript 這個變數的型別，來使用相關的函式或變數
if ( userInputElement ) {
  ( userInputElement as HTMLInputElement ).value = "Hi there!"
}
```

### Index Properties
```typescript=
interface ErrorContainer {
  [ prop: string ]: string;
}

// 可以接受 key 值為 string, value 值為 string 的 n 個屬性
const errorBag: ErrorContainer = {
  email: "Not a valid email",
  username: "Must start with a capital character!"
}
```

### 函式多載( Function Overload )
```typescript=
type Combinable = number | string

// 函式多載要求最下方的函式定義的參數型別及回傳型別
// 需要能兼容上面所有的多載方法
function add( a: string, b: string ): string;
function add( a: number, b: string ): string;
function add( a: string, b: number ): string;
function add( a: Combinable, b: Combinable ) {
  if ( typeof a === "string" || typeof b === "string" ) {
    return a.toString() + b.toString();
  }
  
  return a + b;
}
```

### Optional Chaining
`ES6內容 跳過`

### Nullish Coalescing
`ES6內容 跳過`

## Generics( 泛型 )

### What is Generics ?
```typescript=
/**
 * 當我們希望我們定義的資料型態
 * 能對廣泛不同類型做相同的處理
 * 這邊要如何定義"廣泛不同類型"
 * 就需要使用到 Generics ( 泛型 )
 * 即為 C++ 的泛型
 */

/* 範例 */

// Array 可以接受多種型別
const names: Array<string> = ["hello", "world"]; // 效果等同 string[]

// Promise 回傳可以有多種型別
const promise: Promise<string> = new Promise(( resolve, reject ) => {
  setTimeout(() => {
    resolve( "This is done !" )
  }, 1000) 
});

promise.then( data => {
  // 若無法手動定義 resolve 回傳的型別，則無法知道回傳值的任何參數或方法
  data.split(" ")
})
```

### Generics on Function
```typescript=
function merge<T, U>( objA: T, objB: U ) {
  return Object.assign( objA, objB )
}

// TypeScript 會自動抓取參數的型別作為泛型的型別
// typeof T === { "name": string }
// typeof U === { "age": number }
const mergedObject = merge( { name: "Max" }, { age: "30" } )

// 因為已經知道 T 的型別了，所以可以直接呼叫 .name
console.log( mergedObject.name )
```

### 泛型型別約束
```typescript=
function merge<T extends object, U extends object>( objA: T, objB: U ) {
  return Object.assign( objA, objB )
}

// 第二個參數報錯
// 因為 U 不接受 object 以外的型別
const mergedObject = merge( { name: "Max" }, 30 )
```

### 使用 Interface 來更精準的約束
```typescript=
// 保證有名叫 length 的參數
interface Lengthy {
  length: number
}

function countAndDescribe<T extends Legnthy>( element: T ) {
  let descriptionText = "Got no value"
    
  // 需要有 element.length
  if ( element.length > 0 ) {
    descriptionText = `Got ${ element.length } value${ element.length === 1 ? "" : "s" }.`
  }
}
```

### keyof 關鍵字
```typescript=
// 把從 T 得到的 keys 視為一個子集合，確保 U 屬於該子集合
function extractAndConvert<T extends object, U extends keyof T>(
  obj: T,
  key: U
) {
  return "Value: " + obj[ key ]
}
```

### Generics on Class
```typescript=
// example
class DataStorage<T extends string | number | boolean> {
  private data: T[] = []

  addItem( item: T ) {
    this.data.push( item )
  }

  removeItem( item: T ) {
    if ( this.data.indexOf( item ) === -1 ) {
      return
    }
    this.data.splice( this.data.indexOf( item ), 1 )
  }

  getItems() {
    return [ ...this.data ]
  }
}
```

### Utilitiy Types

#### Partial

* ```typescript=
  interface CourseGoal {
    title: string,
    description: string,
    completeUtil: Date
  }
  
  function createCourseGoal(
    title: string,
    description: string,
    date: Date
  ): CourseGoal {
    let goal: Partial<CourseGoal> = {}
      courseGoal.title = title
      courseGoal.description = description
      courseGoal.completeUtil = date
    return goal
  }
  ```
* Partial 會將其所對應的型別，將裡面的參數全部設定成 Optional。
* 可以確保給呼叫參數並賦值時，不會回報該參數可能為未定義的錯誤

#### Readonly

* ```typescript=
  const names: Readonly<string[]> = ["Max", "Anna"]
  names.push("Mahumahu") // 會出現錯誤
  ```
* Readonly 可以確保對應的變數是不能再透過任何方式修改的，包含增加元素或修改。
* 以原生 JavaScript 來說，names 雖然被定義成 const，但它仍然是可以透過函式修改其值的
* Readonly 則可以排除這個問題

### 

## Decorators