# 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;
}
```
> 補充:透過 `--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
```

託 `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', '!' ]
```