# TypeScript 🖖 ###### Tags: `TypeScript` TypeScript 是 JavaScript 的超集 ( TS 有 JS 所有的功能 與 自己的技術,主打技術 - 型別系統,是一門強型別的語言,透過事先定義型別的模式,彌補了 JavaScript 太過自由導致後續容易發生型別不對應產生的 bug;TypeScript 也支援編譯成不同版本的 JavaScript,能夠在各個瀏覽器執行。 TypeScript 在 State of JS 2020 獲頒 最多人使用獎,高使用率與高滿意度讓大家蜂擁而上的學習這門技術。 ## 為何選擇 TS ? - 型別系統 `開發期間即定義好型別,避免因為沒有定義型別而產生無預期的錯誤。` `e.g. JS => 1 + '1' = '11',在 TS 若是先定義好 param 型別為 number,即可避免後續問題。` - 自由設定要編譯成什麼版本的 JS - 撰寫期間即提示錯誤 - 前端框架支援 ( e.g. Vue 3 - 多個 IDE 支援 - 社群日漸成熟 ## 可能會遇到的難關 - 對於寫 JS 的前端工程師有一定的學習成本 - 開發時要多花一些成本,但對於後續的維護是值得的。 ## 安裝 ```shell npm install -g typescript ``` ```shell tsc -v ``` ## 開始撰寫 > 1. 撰寫一段 TS 的語法 ```typescript // index.ts function sum(a: number, b: number):number { return a + b } ``` > 2.1 透過 `tsc <路徑/檔案>` 編譯 `指定的檔案` ```shell tsc ./index.ts ``` OR > 2.2 透過 `tsc` 編譯當前路徑下的所有 TS 檔 ```shell tsc ``` > 3. 自動產生出編譯後的 JS ```javascript // index.js "use strict"; function sum(a, b) { return a + b; } ``` &nbsp; > 補充:透過 `--watch / -w` 自動偵測異動並進行編譯 ```shell tsc -w ``` ## 高速開發 ts - 透過 nodemon & concurrently 達成 > 1. 裝一下該裝的東東 ```shell npm init -y // 初始化一個 node 專案 tsc --init // 初始化建立 tsconfig.json npm i nodemon concurrently --save-dev // 安裝 nodemon & concurrently ``` > 2. 建立 `dist` 及 `src` 兩個資料夾, `src` 用來放 TS, `dist` 用來放編譯後的 JS > 3. 將 `tsconfig.json` 中的 `outDir` 及 `rootDir` 設定好 ```json … "outDir": "./dist", /* Redirect output structure to the directory. */ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ … ``` > 4. 設定 `package.json` 中的 `script` ```json … "scripts": { "start:build": "tsc -w", "start:run": "nodemon ./dist/index.js", "start": "concurrently npm:start:*" }, … ``` :::info 可以自行獨立 run `tsc -w` 及 `nodemon ./dist/index.js`看看,再體驗 `concurrently` 帶來的好處。 ::: > 5. 開始執行 ```shell npm start ``` ![](https://i.imgur.com/LncyCwL.png) 託 `concurrently` 所賜,此時我們可以發現若是更新 TS 後儲存,會立刻幫我進行編譯 ( `tsc -w` ) 與執行 ( `nodemon ./dist/index.js` )。 ## 資料型別 TS 的資料型別分為 `原始型別` 與 `物件型別`。 ### 原始型別 - number - string - boolean - undefined - null - symbol ### 任意值 若變數在定義時,冠上型別 `any` ,此變數可以為任意型別且不報錯。 ```typescript let val: any = '123' val = 50 ``` 若變數在定義時,並沒有指定型別,TypeScript 會將此變數視為 `any` 型別。 ```typescript let val // 在 typescript 眼裡 -> let val: any val = 1 val = '123' ``` > 太依賴型別 any 並不是一件好事,斟酌使用。 ### 型別推論 若變數在定義時,並沒有指定型別但有指定 val,TS 會自行推論出此變數的型別,若之後修改此變數時型別錯誤則會報錯。 ```typescript let val = '123' // 在 typescript 眼裡 -> let val: string = '123' val = 1 // Type 'number' is not assignable to type 'string'. ``` ### 聯合型別 / Union Types 型別可以定義多個,使用 `|` 來區隔。 ```typescript let val: string | number = '123' val = 1 // work ! val = false // Type 'boolean' is not assignable to type 'string | number'. ``` 上例 val 可以為 string 、 number,若我們要事先撰寫程式對 val 進行處理,必須使用這些型別共同有的屬性。 ```typescript function getLength(val: string | number): number { return val.length } // Property 'length' does not exist on type 'string | number'. Property 'length' does not exist on type 'number'. ``` 可以這麼解決: ```typescript function getLength(val: string | number): number { return val.toString().length } ``` 我們也可以設定好聯合型別提供使用 ```typescript type numOrString = number | string let val: numOrString val = 10 val = 'abc' ``` ### 物件型別 你可以使用以下方式簡易的建立物件: ```typescript let obj: object = { id: 1, name: 'chenyo', } ``` 也可以透過 interface 來定義物件型別: #### 確定屬性 確定屬性就是定義完 interface 後確定會有的欄位,定義了幾組 key value,物件就必須符合定義,有多有少都會報錯! ```typescript interface Customer { phoneid: number name: string gender: string time: Date } const chenyo: Customer = { phoneid: 123456789, name: 'chenyo', gender: 'male', time: new Date("2021-07-26") } ``` > 可以在 interface 的名稱前面加大寫 I,避免把變數與介面搞混。 ( e.g. I_Customer / ICustomer > interface 內個別屬性定義後面不須加 `,` ,但加了好像也沒關係 (? #### 可選屬性 / Optional Properties 在 key 後面加上 `?`,此欄位即為選填。 ```typescript interface Customer { phoneid: number name: string gender: string time: Date tips?: string // 不見得要有此組 key value } const chenyo: Customer = { phoneid: 123456789, name: 'chenyo', gender: 'male', time: new Date("2021-07-26") } ``` #### 任意屬性 `[propName: key 型別]: value 型別` ```typescript interface Customer { phoneid: number name: string gender: string time: Date [propName: string]: any } const chenyo: Customer = { phoneid: 123456789, name: 'chenyo', gender: 'male', time: new Date("2021-07-26"), feeling: 'happy' } ``` #### 唯獨屬性 在 interface 建立時,在 key 前面加上 `readonly` ```typescript interface Customer { readonly phoneid: number name: string gender: string time: Date } const chenyo: Customer = { phoneid: 123456789, name: 'chenyo', gender: 'male', time: new Date("2021-07-26") } chenyo.phoneid = 123 // Cannot assign to 'phoneid' because it is a read-only property. ``` ### 陣列型別 以下三種方式都可以建立陣列,但第三種使用 interface 的較少用。 ```typescript let array: number[] array = [1, 2, 3, 4] ``` ```typescript let array: Array<number> array = [1, 2, 3, 4] ``` ```typescript interface numArray { [index: number]: number } let array: numArray = [1, 2, 3] ``` ### 函式型別 可以透過 `函式陳述式` 或是 `函式表達式` 來進行函式的宣告。 輸入及輸出都要進行型別的管制! #### 函式陳述式 Function Statement ```typescript function stringSum(x: string, y: string, z: string): string { return x + y + z } ``` #### 函式表達式 Function Expression ```typescript const stringSum = (x: string, y: string, z: string): string => x + y + z ``` 函式的引數定義了幾個,呼叫函式時就必須代幾個,或多或少都不行,除非有預先設定了 `可選引數` 或 `預設引數`。 #### 可選引數 與前述可選屬性一樣,在 key 的後面加上 `?`。 ```typescript const stringSum = (x: string, y: string, z?: string): string => x + y + z console.log(stringSum('Hello ', 'World')) // Hello World ``` > 可選引數必須放在引數的最後面,否則會報錯! ```typescript const stringSum = (x: string, y?: string, z: string): string => x + y + z // A required parameter cannot follow an optional parameter. ``` #### 預設引數 若是為引數添加預設值,呼叫函式時可不填。 ```typescript const stringSum = (x: string, y: string, z: string = '!'): string => x + y + z console.log(stringSum('Hello ', 'World')); // Hello World! ``` 當然也可以所有的引數都定義預設值,直接呼叫函式不帶引數也可以運行 ```typescript const stringSum = (x: string = 'Hello', y: string = ' World' , z: string = '!'): string => x + y + z console.log(stringSum()); ``` 雖說預設引數並沒有像可選引數一樣有著必須放在確定引數的後方,但可能還是會出錯,如下: ```typescript const stringSum = (x: string = 'Hello', y: string , z: string = '!'): string => x + y + z console.log(stringSum('World'); // not work ``` 這邊只有 y 並沒有預設引數,若是想印出 'Hello World!' 則必須呼叫: ```typescript console.log(stringSum('Hello', 'World'); // work ! ``` #### 剩餘引數 使用 `ES6` 的 `...rest` 來獲取函式的剩餘引數。 ```typescript function sayIt(first: string, second: string, ...items: number[]): void { console.log('first param:', first); console.log('second param:', second); items.forEach(item => { console.log(item); }); } sayIt('我是第一個', '我是第二名', 5, 4, 8, 7) // first param: 我是第一個 // second param: 我是第二名 // 5 // 4 // 8 // 7 ``` 若是在 `tsconfig.json` 中把 `target` 設定為 ES6 以下的版本時,ES6 的 `...rest` 會被編譯成以下: ```javascript "use strict"; function sayIt(first, second) { var elses = []; for (var _i = 2; _i < arguments.length; _i++) { elses[_i - 2] = arguments[_i]; } console.log('first param:', first); console.log('second param:', second); elses.forEach(function (item) { console.log(item); }); } sayIt('我是第一個', '我是第二名', 5, 4, 8, 7); ``` #### 過載 過載能讓我們依照不同類型的引數,執行到不同的處理流程。 如下例子,我們無法確定傳入的參數型別,這也導致我們無法明確知道結果。 最後我們針對 result 進行 split,看似正常,但 `numOrString` 與 `number` 並不存在 split 的方法,而導致報錯。 ```typescript type numOrString = string | number function sum(x: numOrString, y: numOrString): numOrString { if (typeof x === 'string' || typeof y === 'string') { return x.toString() + y.toString() } else return x + y } const result = sum('Hello', ' World!'); console.log(result.split('')); // Property 'split' does not exist on type 'numOrString'. // Property 'split' does not exist on type 'number'. ``` 這邊我們為此函式設定多組型別引數來進行函式過載。 ```typescript type numOrString = string | number function sum(x: number, y: number): number function sum(x: number, y: numOrString): string function sum(x: numOrString, y: number): string function sum(x: numOrString, y: numOrString): string function sum(x: numOrString, y: numOrString): numOrString { if (typeof x === 'string' || typeof y === 'string') { return x.toString() + y.toString() } else return x + y } const result = sum('Hello', ' World!'); console.log(result.split('')); // [ 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!' ] ```