Try   HackMD

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
TypeScript 基礎篇

Core Types

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
主要常見型別

  • 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 聯合型別
      • ​​​​​​ // example ​​​​​​ function example( a: number | string, b: number | string ) { ​​​​​​ // ...function content ​​​​​​ }
      • 可以一次代表多個型別
      • 多用於函式的參數預定義
    • Literal 字面值型別
      • ​​​​​​ // example ​​​​​​ let example: "hello" ; ​​​​​​ let example_literal_union: "hello" | "world"
      • 變數值將只能指派預先已經設定好的值
      • 接受三種類別作為其成員: string, number, boolean
      • 使用場景類似 switch...case
      • 通常會和 Union 一起使用
    • enum 列舉
      • ​​​​​​ // example ​​​​​​ enum Role{ ADMIN, READ_ONLY, AUTHOR, CUSTOM = 100 }
      • 將數組轉換成易讀的文字使用
    • unknown 未知型別
      • ​​​​​​// example
        ​​​​​​let unknownUserInput: unknown
        ​​​​​​let anyUserInput: any
        ​​​​​​let userName: string
        ​​​​​​
        ​​​​​​// 假設使用者輸入的是一般字串
        ​​​​​​userName = unknownUserInput // 會出現錯誤
        ​​​​​​userName = anyUserInput // 不會出現錯誤
        ​​​​​​
        ​​​​​​// 前者需要經過一層型別檢查才能賦值給其他型別的變數
        ​​​​​​if ( typeof unknownUserInput === "string" ) {
        ​​​​​​  userName = unknownUserInput
        ​​​​​​}
        
      • unknown 設計上雖然比 any 來的好,但仍然不是個該被過分使用的型別。
    • any 軟型別
      • 任何型態的預設值皆屬於這個型別

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
注意!!
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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

TypeScript 預定義型別方法

一般定義

const num_1: number ; // 在變數後打上冒號並追加想要的型別 const str_1: string ; // 通常只會使用在未定義初值,只定義型態的時候 // 因為 tsc 編譯時可以自動抓取你定義的初值的型態,所以不需要手動再給一次型別

函式參數

// 在參數後打上冒號並追加想要的型別 // 在參數後加上問號代表該參數非必要 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() } }

陣列型別定義

let myHobbies: string[]; myHobbies = ["Sports"] // tsc 在處理時,會自動將所有成員的型別認定為您預定義的型別 // 可以保證你在使用相關內建函式時不會出現錯誤

物件成員定義

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 元組定義

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

自定義型別

// 類似 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 寫法

function example( a, b ) { // content... return result }
  • 參數不需要定義型別
  • 函式本身沒有回傳型別

TypeScript 寫法

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

不強制回傳(Void)

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

不回傳(Never)

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

函式(Function)也是一種類別

// 代表任何函式 let listener_a: Function; // 代表需要兩個參數,並且回傳型別為 number 之函式 let listener_b: ( a: number, b: number ) => number; // 參數的名稱並不會造成影響,參數的個數可以向下兼容 listener_b = param => { // 內容... }

Compiler & Configuration Deep Dive

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
TypeScript 編譯器詳細設定介紹

Initialization 專案初始化

  • ​​tsc --init
  • 這個指令將會把現在所在的資料夾視為一個 typescript 的專案資料夾
  • 會加入一個新檔案,名叫 tsconfig.json,包含所有關於 typescript 編譯器的詳細設定

Watch Mode 監看模式

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

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

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

CompilerOptions 介紹

完整設定文件
// 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 } }
更新紀錄

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 型別
      • ​​​​​​// 隱式宣告 param 為 any 型別 ​​​​​​function example( param ) { // 會在這行出現錯誤 ​​​​​​ console.log( param ) ​​​​​​} ​​​​​​ ​​​​​​example("Hello World!")
    • strictNullChecks

      • 強迫開發者檢查變數型別是否為 undefinednull
      • 以防呼叫相關函式的失敗
      • ​​​​​​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
    • ​​​​function example( value ) { ​​​​ if ( value === 0 ) { ​​​​ return 1 ​​​​ } ​​​​ ​​​​ // 需要另外加上 return 關鍵字 ​​​​ // 注意函式結束點不一定是函式結尾 ​​​​ return ​​​​}
  • allowUnusedLabels

    • 允許未使用的標籤的存在
    • ​​​​function verifyAge(age: number) { ​​​​ if (age > 18) { ​​​​ verified: true; ​​​​ } ​​​​}
  • allowUnreachableCode

    • 允許語法上導致無法執行的程式區段的存在
    • type guard 造成的無法執行區段則不受此規則影響

Working with Next-gen JS Code

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
如何在 TypeScript 中使用新潮的 JavaScript 功能

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
本章節主要為 ES6 相關,與 TypeScript 本身相關度較低,故暫時跳過

Classes & Interfaces

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
TypeScript 的類別與介面

First Class

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

根據 class 建立新物件

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

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

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

// 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

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

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

// 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

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

// 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

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

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

// 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

/** * 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

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 的繼承

// 您可以選擇用 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

// 需要有兩個 number 的變數,並回傳 number 型別 interface mathFunction { ( a: number, b: number ) => number; } let add: mathFunction add = ( left: number, right: number ) => { return left + right; }

Advanced Types & TypeScript Features

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
進階的功能與特殊型別

Intersection Types

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

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

// 藉由在 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

<input type="text" id="user-input">

TypeScript

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

Index Properties

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 )

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 ?

/** * 當我們希望我們定義的資料型態 * 能對廣泛不同類型做相同的處理 * 這邊要如何定義"廣泛不同類型" * 就需要使用到 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

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 )

泛型型別約束

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 來更精準的約束

// 保證有名叫 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 關鍵字

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

Generics on Class

// 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

  • ​​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

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

Decorators