[![hackmd-github-sync-badge](https://hackmd.io/J-lzcW66RdiACrDtUa9Xpg/badge)](https://hackmd.io/J-lzcW66RdiACrDtUa9Xpg) ###### tags: `TypeScript` # 【學習筆記】TypeScript 基礎入門:從型別談起 [TOC] 本篇為以下資源之學習筆記: - [Github - microsoft/TypeScript](https://github.com/microsoft/TypeScript) - [TypeScript 新手指南](https://willh.gitbook.io/typescript-tutorial/) - [Angular Tutorial for Beginners: Learn Angular & TypeScript - Programming with Mosh ](https://www.youtube.com/watch?v=k5E2AVpwsko) ## 什麼是 TypeScript? ![](https://i.imgur.com/uowtOyV.png) 根據 [TypeScript 官網](https://www.typescriptlang.org/) 說明: > TypeScript extends JavaScript by adding types. > By understanding JavaScript, TypeScript saves you time catching errors and providing fixes before you run code. > Any browser, any OS, anywhere JavaScript runs. Entirely Open Source. 將上述文字翻譯成中文: > TypeScript 透過增加型別定義來擴展 JavaScript。 > 透過瞭解 JavaScript,TypeScript 可在執行程式碼之前找到錯誤並提供修復,節省開發除錯的時間。 > 可在任何瀏覽器、任何作業系統、任何能運行 JavaScript 的地方執行。並且是完全開源的。 簡單來說,TypeScript 是⋯⋯ - 一個基於 JavaScript 的超集合(SuperSet) - 提供型別系統(Type System),能夠在開發時期宣告型別 - 支援 ECMAScript,可將 TS 檔編譯成 JS 檔給瀏覽器解讀 ![](https://i.imgur.com/nTdyP5I.png) (圖片來源:[Angular TypeScript Vs ES6 Vs ES5](https://www.sneppets.com/angular/typescript-vs-es6-vs-es5/)) ## 型別系統 Type System 由於 JavaScript 是弱型別語言,TypeScript 的出現就是為了解決這個問題。 而 TypeScript 就是原生 JavsScript 的延伸,包含 ES3、ES5 與 ES6+ 語法,以及本身的擴充內容,也就是說,TypeScript 具備以下特性: - Strong typing 強型別 - Object-oriented-features 物件導向特性 - Compile-time errors 編譯錯誤 具體而言,TypeScript 和 JavsCript 的差別在哪呢?以下舉簡單的例子說明: 原生 JavaScript: ```javascript= function sayHello(person) { return 'Hello, ' + person; } let name = 'Heidi'; console.log(sayHello(name)); ``` TypeScript: ```typescript= function sayHello(person: string) { // 傳入的參數 return 'Hello, ' + person; } let name: string = 'Heidi'; // 宣告的變數 console.log(sayHello(name)); ``` 可以發現差別就在於「型別」,關於 TypeScript 如何處理型別,主要可分為這三個概念,也就是所謂的型別系統: * Type annotation(型別註解) * 主動,大多使用在初始化階段,例如宣告變數或函式參數等 * Type Inference(型別推論) * 被動,自動推論資料型別的機制 * Type Assertion(型別斷言) * 主動,通常用於接收外部參數,需明確指定資料型別 ### 型別註解 Type annotation 在上述範例中,TypeScript 透過 Type annotation(型別註解),在參數或變數之後加上冒號 `: type`,繼續以剛才的例子說明: ```typescript= // 變數的型別註解 let name: string = 'Heidi'; // 函式參數/回傳值的型別註解 function sayHello(person: string): string { return 'Hello, ' + person; } ``` 一旦宣告型別,就不能使用其他資料型別進行賦值,否則程式就會報錯(但 TypeScript Compiler 還是會編譯成 JS 檔)。 像這樣藉由 Type Annotation(型別註解)執行靜態型別檢查,可有效預防運行錯誤,並統一規格、提高程式碼的可讀性,以方便多人協作。 ### 複習:JavaScript 型別 JavaScript 的型別分為兩種:原始資料型別(Primitive data types)和物件型別(Object types)。 - 原始資料型別 - boolean 布林值 - number 數值 - string 字串 - null 空值 - undefined 未定義 - Symbol(於 ES6 新定義) - 物件型別 - Object 物件 - Array 陣列 - Function 函式 在 TypeScript 中,除了上述這些型別,還有像是空值(Void)、任意型別(any)、Never 等特殊型別。 ### 空值 Void:沒有回傳值 在 JavaScript 沒有空值(Void)的概念,而在 TypeScript 中,通常用 void 表示沒有任何 return 值的 function: ```typescript= function alertName(): void { alert('My name is Heidi!'); } // 若宣告一個 void 型別的變數,只能賦值為 null 或 undefined let unusable: void = undefined; ``` ### 補充:void 與 never 的差異 - Void 型別 - 沒有回傳值,函式會繼續執行到結束 ```typescript= let noReturn = function sayHello(){ console.log('hello'); // 不會有回傳值 } ``` ![](https://i.imgur.com/POJa4wH.png) - Never 型別 - 應該要回傳,但因為函式中斷執行或進入無窮迴圈,永遠不會有回傳值的函式 - 常用於處理函式的錯誤 ```typescript= let neverEnd = function forever(){ while(true){ // 無窮迴圈 // code } } ``` ![](https://i.imgur.com/RNQgOoe.png) 參考資料:[【Day 15】TypeScript 資料型別 - 特殊型別(上)- Never](https://ithelp.ithome.com.tw/articles/10222916) ### 任意型別 any:不檢查型別 其實就類似在 JavaScript 使用 var 宣告變數。一旦將變數宣告成 any 型別,或在定義時沒有賦值,不管之後有沒有賦值,都會被推斷成 any 型別,完全不會進行型別檢查: ```javascript= let myFavoriteNumber; myFavoriteNumber = 'seven'; myFavoriteNumber = 7; ``` ![](https://i.imgur.com/u8oOgg5.png) > 注意:不要濫用 any 任意型別,否則將失去型別檢查保障! ### 陣列型別 Array 表示陣列的方式,大致可分為以下幾種: - 型別 + 方括號:`type[]` - 陣列泛型(Array Generic):`Array<elemType>` - 用介面表示陣列 - 類別陣列(Array-like Object) 比較簡單,也較常使用的方法是以「型別 + 方括號」來表示陣列: ```typescript= let fibonacci: number[] = [1, 1, 2, 3, 5]; ``` 一旦宣告型別,陣列的項中就不允許出現其他型別,否則會報錯: ```typescript= let fibonacci: number[] = [1, '1', 2, 3, 5]; // Type 'string' is not assignable to type 'number'. ``` 常使用 any 來表示陣列中允許出現任意型別: ```typescript= let list: any[] = ['heidiliu', 99, { website: 'https://github.com/heidiliu2020' }]; ``` 我們也可以使用陣列泛型(Array Generic) `Array<elemType>` 來表示陣列: ```typescript= let fibonacci: Array<number> = [1, 1, 2, 3, 5]; ``` ### 列舉型別 enum 以下方程式碼為例: ```typescript= enum Color {Red, Green, Blue}; // 0, 1, 2 let c: Color = Color.Green; // 1 ``` 經過 TSC(TypeScript Compiler)後,會得到下方結果: ```javascript= "use strict"; var Color; (function (Color) { Color[Color["Red"] = 0] = "Red"; Color[Color["Green"] = 1] = "Green"; Color[Color["Blue"] = 2] = "Blue"; })(Color || (Color = {})); ; let c = Color.Green; ``` 接著以番茄鐘為例,假設 Timer 有三種狀態,分別是停止、暫停、計時,即可列舉下列複合型別: - timer-status.enum.ts:進行宣告並輸出 ```typescript= export enum TimerStatus { STOP = 'STOP', PAUSE = 'PAUSE', COUNTING = 'COUNTING' } ``` - pomodoro.component.ts:引入使用,用法和物件類似 ```typescript= import { TimerStatus } from 'src/app/timer-status.enum'; // ... ngOnInit(): void { this.displayTime(); console.log(TimerStatus); console.log(TimerStatus.STOP); } // {STOP: "STOP", PAUSE: "PAUSE", COUNTING: "COUNTING"} // STOP ``` ### 型別推論 Type Inference 在 TS 檔中檢視上述範例程式碼,會發現不管有無明確註記型別,TypeScript 編譯器都會依照「型別推論」的規則,自動推斷出一個資料型別。 又以下方程式碼為例,雖然沒有指定值的型別,卻會在編譯時報錯: ```javascript= let myFavoriteNumber = 'seven'; myFavoriteNumber = 7; // index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'. ``` 這是因為在宣告變數並賦值時,TypeScript 編譯器就已經自動從程式碼推斷出 myFavoriteNumber 是字串型別,若嘗試以非字串的資料賦值時就會報錯! 對 TypeScript 編譯器來說,上方程式碼其實就等同於: ```javascript= let myFavoriteNumber: string = 'seven'; myFavoriteNumber = 7; // index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'. ``` 由於型別推論是 TypeScript 被動的防護機制,不管在什麼情況下都會自動推論型別,即使想故意註記一個錯誤,透過 TypeScript 的運算機制,仍會跳出錯誤訊息: ```javascript= let randomNumber: string = Math.random() /* TS2322: Type 'number' is not assignable to type 'string'. */ ``` ### 型別斷言 Type Assertion 但實際在開發時,專案的資料型別也會越發複雜,不能只仰賴 TypeScript 單純的推論,這時就需要適時搭配型別斷言。 開發者能藉由手動指定一個值的型別,覆蓋 TypeScript 編譯器的推論,避免出現警告錯誤,這個機制就是「型別斷言」。 型別斷言有兩種寫法: - 第一種:`<型別>值 `(angle-bracket <>) ```typescript= let code: any = 'Hello'; let helloCode = <string> code; ``` - 第二種:`值 as 型別`(as keyword) ```typescript= let code: any = 'Hello'; let helloCode = code as string; ``` 兩者語法效果相同,但如果是在 React 專案中,使用 JSX 語法時只能用第二種。 - 使用範例:將聯合型別的變數指定為更加具體的型別 在聯合型別的情境下,表示值可以是多種型別的其中一種。以下方程式碼為例,getLength 的參數可能是字串或數字,但因為 length 不為字串和數字的共同屬性,因此會報錯: ```typescript= function getLength(something: string | number): number { if (something.length) { return something.length; } else { return something.toString().length; } } // error TS2339: Property 'length' does not exist on type 'string | number'. // Property 'length' does not exist on type 'number'. ``` 這時可使用型別斷言,將 something 指定為字串型別(如:`<string>something`),即可避免報錯: ```typescript= function getLength(something: string | number): number { if ((<string>something).length) { return (<string>something).length; } else { return something.toString().length; } } console.log(getLength(12345)) // 5 console.log(getLength("abc")) // 3 ``` > 參考資料:[【Day 04】 TypeScript 判斷資料型別的機制 - 型別推論 x 斷言 x 註解](https://ithelp.ithome.com.tw/articles/10217384) <br> ## TypeScript 和 ES6+ 的差別? 接下來,在深入探討 TypeScript 與 ES6+ 差別之前,先來稍微回顧一些常用的 ES6+(或稱 ES2015)語法吧! ### 複習:那些常用的 ES6+ 語法 * let 與 const 宣告變數,以及變數作用域的不同 * var:function scope 函式作用域 * let 與 const:block scope 區塊作用域 * 解構賦值(Destructuring assignment) * 能夠快速建立變數並取值 ```javascript= const pcBrands = ['Apple', 'Lenovo', 'Acer']; // 自動建立名為 first, second, third 的變數 // 再依照陣列內元素的順序把值取出來 const [first, second, third] = pcBrands; console.log(first); // Apple console.log(second); // Lenovo console.log(third); // Acer ``` * 箭頭函式(Arrow Functions) ```javascript= const foo = () => { console.log("ES6"); } function foo2() { console.log("ES5"); } foo(); // "ES6" foo2(); // "ES5" ``` * 模板字串(Template Literals) * ES5:使用單引號('')或雙引號("") * ES6:使用反引號(``) ```javascript= // 可用於字串拼接 console.log("Hello, " + name) console.log(`Hellooo, ${name}`) ``` * 展開語法和其餘語法(Spread Syntax/Rest Syntax) * 根據不同用途使用 `...` 運算子 #### 展開語法:解壓縮,常用於複製一個物件或陣列,並為該物件增加屬性、或陣列添加元素 ```javascript= const PCOnSale = ['Apple', 'Lenovo', 'Acer']; const allPCs = [...PCOnSale, 'HP', 'ASUS', 'TOSHIBA']; console.log(allPCs); // ["Apple", "Lenovo", "Acer", "HP", "ASUS", "TOSHIBA"] ``` #### 其餘語法:壓縮,把沒有被取出來的物件屬性或陣列元素都放到一個壓縮包裡 ```javascript= const PCBrands = ["Apple", "Lenovo", "Acer", "HP", "ASUS", "TOSHIBA"] ; const [first, second, third, ...other] = PCBrands; console.log(other); // ["HP", "ASUS", "TOSHIBA"] ``` - Default Parameters 設定參數預設值 - Import & Export 引入與輸出 - 引入與輸出 module,類似 require 與 module.exports 的用法 > 可參考:[[Day 02] React 中一定會用到的 JavaScript 語法](https://ithelp.ithome.com.tw/articles/10217085) ### 所以,TypeScript 特別在哪? 在複習了 ES6+ 語法之後,究竟 TypeScript 除了型別系統,還提供哪些重要的酷東西呢?為什麼不寫 ES6+ 就好? 主要可分為三個方面: * Class 類型 * Interface 介面 * 未來的 ES2016+ 特性,例如:Annotations 註解, Decorators 裝飾器, async/await 異步/等待 ### 介面 Interface:定義抽象物件的型別 Interface 被稱作介面或是接口,在物件導向程式語言中,用來定義抽象物件的型別,又被稱作是 TypeScript 的一個型別檢查工具。 > 因為介面只做描述,不做動作。 主要用於定義 Class(類別)行為,介面只會描述有哪些 Method(方法)和 Property(屬性),不包含怎麼執行,也就是說,具體行為必須由 Class 實現(implement)。 > 賦值的時候,Class 需和介面定義的行為保持一致。 舉例來說: ```typescript= // 定義一個介面 Person interface Person { name: string; age: number; address?: string; // 可選屬性 } // 定義一個變數 heidi,型別是 Person let heidi: Person = { name: 'Heidi', age: 99 }; ``` 當 Class 用 implements 指定要實作的 Interface,除了可選屬性(在屬性名稱後方加上問號 `?`),必須實作介面內所有的 Method 和 Property,否則會在編譯時報錯。 ## 結語 這篇是在剛接觸 Angular 框架時,寫下的學習筆記。偶爾會被問說,覺得學新框架最大的困難處在哪?其實 Angular 和 React 要說不同,也不盡然完全相差甚遠,或許實作方法不一樣,卻還是能用共通的邏輯去思考問題。 反而時常會卡關的地方,是在 TypeScript 型別判定上,為什麼這裡型別檢查這麼嚴格?難道我一定要給個 any 才會過嗎?直到學會如何定義 Interface 之後,後續開發就會順利不少,尤其是在打電文時更是如此,能事先統一 Request 和 Response 的格式,就比較不容易出現意外的錯誤。