0%

經常使用 vue 或 React 等前端框架來進行專案開發,但沒有機會好好來認識下他背後的打包工具:webpack
趁著這次六角釋出的基礎 webpack 教學,順道來記錄一下。

前端壓縮打包工具

  • 從官網的圖,很明顯地就可以看到將左側複雜的檔案進行壓縮,並做項目管理
  • 在 Vue , React 的背後都有使用 webpack

    使用 webpack

  1. 安裝
    建立 webpack 需要使用到 npm 初始化
    npm init -y
    安裝之後,會產生 package.jason 檔案

  2. 安裝 webpack 以及 webpack-cli ,並存在 開發環境
    npm install webpack webpack-cli --save-dev

    • 會產生 package-lock.json => 紀錄版本詳細資訊
    • node_modules 資料夾

進入點(entry)、輸出點(output)

  • /src/index.js
  • /dist/main.js
  • 指令:"build": "webpack" => 設定 webpack 將 src 中的 index.js 檔案輸出,放到 dist 資料夾
    • 使用該指令 npm run build
      • build 之後產生 dist/main.js

entry, output 名稱修改

* 建立 `webpack.config.js` 檔案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require('path');

module.exports = {
//預設路徑位置
entry: './src/index.js',
output: {
//輸出位置名稱(filename),檔案位置
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
};

//path.resolve(__dirname, 'dist')
// dirname當前資料夾路徑
//path.resolve 將相對路徑改為絕對路徑

讓每次產生的 bundle 名稱不一樣

  • 搭配 plugin 來使用 HtmlWebpackPlugin

NPM script , 自訂指令

  • 用 node 執行一段 JS ,位置為同層下的 hello.js
  • npm run hello
  • 網站上線\部署或開發,可能都需要下不同指令來進行管理

mode 切換

  • 不同模式下,會因程式內容而輸出不同內容
  • production 上線版模式, 輸出的 bundle 檔案會較為精簡
    • 會壓縮優化,將程式碼壓成一行
    • 不容易除錯與理解

CSS loader

  • 幫助 webpack 載入 css
  • npm install --save-dev css-loader
  • CSS loader
  • webpack.config.js
  • 別忘了 在載入點加入 ·import css from './all.css'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const path = require('path');

module.exports = {
//預設路徑位置
entry: './src/index.js',
output: {
//輸出位置名稱(filename),檔案位置
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.css$/i, //只要副檔名為 .css 就會使用以下
use: ["style-loader", "css-loader"],
},
],
},
};

載入 webpack 測試伺服器

1
2
3
4
5
6
7
8
9
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
//contentBase: path.join(__dirname, 'dist'),
},
compress: true,
port: 9000,
open: true //可以即時更新
},
  • 最後,指令調整,如此就可以開始進行編譯

  • webpack 也可以載入 node_module 下的東西

  • 用 dist 下的檔案來部署

    webpack.config.js 檔案

  • 注意

  • mode: 設定development 或是 production

  • entry 進入點在哪

  • output 輸出去哪

  • module -> rule 下 會放 scss-loader , babel-loader 等規則

  • plugins: webpack 主要是讀檔案,但透過 plugins ,如建立新的 html檔案, MiniCssExtractPlugin:將 css 從 js 抽離出來

  • devtool: ‘source-map’ => 協助檔案 debug

關於 Webpack,它是什麼?能夠做什麼?為什麼?怎麼做?— freeCodeCamp 的筆記
Webpack 是什麼?模組打包工具的用途及基本 Webpack 教學

props, state 的應用

  • props 傳入參數的部分
  • state 讓組建本身有狀態

製作 計數器

  • props
  • 製作,讓計數器的數字變化,可以同步變化到 button 上的數字
    • 在父層 App.vue 做一個內容的傳送
    • 在 子元件,要自訂一個變數,作為接收父層從來的資料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "./styles.css";

type BtnProps = {
btnNum: number;
};

const Btn: React.FC<BtnProps> = ({ btnNum }) => {
return (
<button>
add 1<span>目前總數:{btnNum}</span>
</button>
);
};

export default function App() {
const num = 0;
return (
<div className="App">
<h1>Counter: {num}</h1>

<Btn btnNum={num} />
</div>
);
}


進階,希望透過點擊 button 同步更改數值

  • 讓 render (react 渲染機制),可以在變數被變動的時候順便觸發渲染機制

    • 改變變數 -> 觸發渲染

      使用 state 達成上述目的

    對組建本身引入 state 機制

  • 用 state 來做變數宣告,只要 state 變數有做變動都會觸發渲染機制

    • 當變數有涉及於頁面上的更新,就會需要使用 state
  • Hook 本身多是 use 開頭

  • useState() 是一個 function

用陣列解構來取的 return 的值

    1. 當前的值
    1. 再回傳一個 function : 用來設定/變動 第一個數值
      const [num2, setNum2] = useState(999)

example-1

實現 父層數值變化,子組件隨之變化

  • 應該將主要的更新動作都放置父組件
  • 將放在父組件的事件,透過 props 方式傳給子組件,並將該變數放到按鈕的點擊事件上

example-2

剛學習 vue 的時候,對 slot 完全一竅不通,不了解要如何挖空多元件,來放入資料,更是不知道他能使用的時機是什麼時候,種種因素,讓我從官方文件找些範例釐清,也找了些文章閱讀加深記憶,順手也紀錄在部落格上~

使用時機:

  • 使用同一個 元件 , 但只有內文不同。
  • 有時候資料是靜態,不常變動,甚至需要大量重複。這情況不必要使用 props。
  1. 共用樣式
    官網的範例可以看到,讓按鈕的文字可以彈性的變化。

  2. named slot(具名插槽)

  • 建立一個元件,其中含有多個slot區塊
    範例連結
  1. Scoped slots(作用域插槽)
  • 把子元件的資料取出來,給父層使用
  • 不同於前面 slot 的使用,是由父層輸入資料呈現於頁面
    範例

資料來源:
vuejs
不只懂 Vue 語法:什麼是 slot?請示範 slot 的用法?
用範例理解 Vue.js #18:Slot

近期購買了布魯斯的 TypeScript 的課程,除了學到型別之外,還接觸到 WebSocket 的使用,所以就來簡單紀錄下 WebSocket 是什麼,在專案內可以實現什麼功能。

WebSocket是一種網路傳輸協定,讓前端與後端溝通除非有一方斷開連接,否則會一直保持聯繫

情境說明

  1. 使用者上下線狀態:可以延遲幾秒鐘,可以使用 setInterval 做出輪循
  • 設定每幾秒發出請求,更新狀態
  • 使用 HTTP 協議的請求:GET\POST 等方式,做出Request\Response
  1. 聊天室:會希望聊天訊息是即時更新,若採用上面的方式,為了使訊息更新及時會從每幾秒改為每幾毫秒,這樣的方式會造成短時間內不斷發出請求,造成消耗後端資源
  • 指建立一次連結,可以與後端持續保持聯係

聊天室的實際運用

  • 將在 node.js 建立 websocket 的服務,讓前端連線到node.js的服務
  • socket.io => 包裝 websocket 協議的 node.js 的工具包
    • 當使用 node.js 做開發,就可以透過 socket.io 提供的功能實現

安裝 socket.io

  • socket.io-clinet : 在前端的部分要使用 clinet 安裝包
    npm i -D socket.io socket.io-clinet

後端

  • 將 socket.io 引入 node.js 的 server 端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import devServer from "@/server/dev";
import prodServer from "@/server/prod";
import express from "express";
import { Server } from "socket.io"
import http from "http" //也可以引入http模組

const port = 3000;
const app = express();
//透過 http 將 server 建立起來
const server = http.createServer(app)
const io = new Server(server)
const userService = new UserService

//監聽
server.listen(port, () => {
console.log(`The application is running on port ${port}.`);
});

1
2
3
4
5
6
//建立起連接時,發出訊息
//當有用戶連接到connection 就會有回呼函式
io.on('connection',(socket)=>{
//發出訊息
socket.emit('join', 'Welcom')
})

如何在前端使用,與後端建立連線?

  • 到前端的入口檔案
1
2
3
4
5
6
7
8
9
10
//將 socket.io 的包引入前端程式這邊
import { io } from "socket.io-client";

//1. 建立連接到 node server
const clientIo = io();


clientIo.on('join',(msg)=>{
console.log('msg',msg)
})

toRef

  • 將…變成 ref
    從 reactive 物件抽出包裝成一個 ref 的值,與原本物件 是連接的,所以修改時,原本的值也會跟著變動。
  • 只能處理一個屬性,須告知要輸入的屬性\物件
  • 注意: 更改原始物件內的值,所響應會跟著變動

在 prop 的應用

  • 來自的 props 是 物件資料,可以使用toRef
    1
    2
    3
    4
    5
    6
    7
    8
    9
    props: {
    apiUser: {
    required: true,
    type: Object
    }
    },
    setup(props) {
    const userCopy = toRef(props, 'apiUser')
    }
  • 範例二
1
2
3
4
5
6
7
8
9
10
<template>

<h2>姓名: {{person.name}}</h2>
<h2>年齡: {{person.age}}</h2>
<h2>職業描述: {{person.job.front.dec}}</h2>
<button @click='person.name += ~'>修改姓名</button>
<button @click='person.age ++ '>增加年齡</button>

</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
import {reactive} from 'vue';

export default {
name: 'Demo',
setup(){
const person = reactive({
name: 'Eva',
age: 25,
job:{
front:{
dec:'前端工程師'
}
}
})

return{
person
}
}
}
</script>
  • 要將以上的模板寫的更容易
1
2
3
4
5
6
7
8
9
10
<template>

<h2>姓名: {{name}}</h2>
<h2>年齡: {{age}}</h2>
<h2>職業描述: {{dec}}</h2>
<button @click='name += ~'>修改姓名</button>
<button @click='age ++ '>增加年齡</button>

</template>

1
2
3
4
5
6
//就是返回普通的物件內容
return{
name: person.name,
age: person.age,
dec: person.job.front.dec
}
  • 但此方式要注意,字串模板會無法響應更新的問題

  • 使用 toRef 來達到響應

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
import {reactive,toRef} from 'vue';

export default {
name: 'Demo',
setup(){
const person = reactive({
name: 'Eva',
age: 25,
job:{
front:{
dec:'前端工程師'
}
}
})

//單一轉換
const name2 = toRefs(person,'name')

return{
name: toRef(person,'name'),
age: toRef(person,'age'),
dec: toRef(person.job.front,'dec')
}
}
}
</script>

toRefs

toRefs用來把響應式物件轉換成普通物件,把物件中的每一個屬性,包裹成ref物件
* toRefs就是toRef的升級版,只是toRefs是把響應式物件進行轉換

延續上方,toRefs

  • 可以批量處理,一個物件裡的屬性
    • 只會將第一層展開,所以以下在 job 的內層
1
2
3
4
5
6
7
8
9
10
<template>

<h2>姓名: {{name}}</h2>
<h2>年齡: {{age}}</h2>
<h2>職業描述: {{job.front.dec}}</h2>
<button @click='name += ~'>修改姓名</button>
<button @click='age ++ '>增加年齡</button>

</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
import {reactive,toRefs} from 'vue';

export default {
name: 'Demo',
setup(){
const person = reactive({
name: 'Eva',
age: 25,
job:{
front:{
dec:'前端工程師'
}
}
})

//可以批量處理,所以只需要傳入個物件,
const new = toRefs(person)
console.log(new)

//在物件中再放toRefs(person) ,會變成物件包物件,所以要展開
return{
...toRefs(person)
// name: toRef(person,'name'),
// age: toRef(person,'age'),
// dec: toRef(person.job.front,'dec')
}
}
}
</script>

從開始學習前端之後,大多的學習資源都是來自線上課程、youtube 影片為主,鮮少透過閱讀書籍來學習相關知識,但自從進入公司之後,因為公司內有些程式書籍能夠借閱,先是從 JavaScript 大全開始,一開始閱讀起來真的滿痛苦,不知道該如何消化內容,直接順著讀過去又怕忘記,但目前還是順順的讀到了第8章~後來,找到的方式就是在筆記軟體上做些註記,工作上遇到相關的再回過頭看一次,加入筆記。

而在此次,發現 TypeScript 即便已經看過介紹型別、基本使用基礎的影片後,仍然覺得不太足夠,所以就開始翻閱書籍 「讓 TypeScript 成為你全端開發的 ACE」,文中介紹滿多之前沒有注意到觀念、也有帶到不少 JavaScript 的觀念來呼應。所以本篇,多是來自於書中資訊,透過整理呈現。

註記 Annotation V.S. 斷言 Assertion

  • 型別註記:指在告訴 TS 編譯器:「任何被註記到的變數、函式的參數等,都必須遵照被註記過後的變數型別」。
  • 所以編譯器會隨時隨地的監測該變數有沒有出現型別衝突的可能—關鍵字為遵照。
  • 而,斷言型別則是無視 TS 編譯器分析整個程式碼的型別推論過程,果斷的告訴 TS 編譯器:「被斷言過後的表達式之運算結果就是某某型別」—關鍵意象是「覆蓋」該表達式的型別推論結果。

註記與斷言

  • 帶有冒號的相關語法
1
2
let randomNumber:number = Math.random();
const subscribed:boolean = true;
  • 如果遇到函式,參數(Argument)部分除了可以有類似註記方式標明輸入的參數型別外,在參數宣告結尾也可以註記該函式輸出之型別。
1
2
3
function isPositive(input: number):boolean{
return input > 0
}
  • 但JS裡,宣告函式的方法有很多種,其中一個是將函式最為值指派到變數。
1
2
3
const isPositive:(input:number) => boolean = function(input){
return input > 0
}
  • 以上,也能把變數指派的函式,改成ES6箭頭函式
    • 指派運算子(Assignment Operator ,就是指程式裡的等號)左方是函式的型別註記表示法,右方則是普通函式宣告
1
const isPositive:(input:number) => boolean = input => input >0;
  • 採用邊宣告函式、邊註記型別的方式定義函式
1
2
3
const isPositive = function (input:number):boolean{
return input>0
}
1
2
const isPositive =  (input:number):boolean =>  input>0

型別斷言語法

  • 斷言(Assertion)的語法很簡單,看到有使用關鍵字 as 或者 <T>(...) 的格式就是斷言的用法。
  • 通常會使用斷言的情境,程式沒辦法推論某表達式的確切運算結果之型別,我們才會用選擇使出斷言來處理這種情境。
  • 程式沒辦法推論?
    • 使用第三方的資源(Third-party resources),如使用外來JSON API 獲得的內容之型別格式、讀取檔案轉成 JSON 物件的結果、使用套件提供的功能、呼叫會回傳未知結果的函式。
1
const aNumber = returnsUnknow() as number;

或是這樣:

1
const aNumber = <number>(returnsUnkonw());
  • 請注意:斷言的語法部分,沒有人斷言在變數的名稱宣告部分—也就是是說,如果你這樣寫是錯的:
1
const aNumber as number = returnsUnknow(); //這是錯誤的
  • 概念有點像,決斷地告訴程式,某表達式的運算結果之型別。
  • 變數本身不是被運算,而是被指派某個東西,而斷言應該是斷在被指派的值或表達式的運算結果上。

複雜一點

  • 函式宣告表達式有可以被斷言(畢竟函式作為表達式也會被當成值)
1
const isPositive =(input => input > 0) as (input: number) => boolean

或者是:

1
const isPositive = <(input: number) => boolean>(input => input >0);
  • 上面斷言的寫法效果跟之前示範過基礎註記手法:
1
const isPositive :(input: number) => boolean = input => input >0;

或者是

1
const isPositive =(input: number):boolean => input >0;

說下,敘述式與表達式的定義與差別

  1. 敘述式:程式運行的流程,例如:JS 裡的判斷敘述式(if…else)以及迴圈敘述式(for 或者是 while 迴圈)。
  2. 表達式代表的則是程式碼運算的流程,並且會將運算結果回傳。其中兩者最關鍵差異為:敘述式不會回傳值,表達式則會。
  • 以下為常見的表達式範例:
1
2
3
4
5
6
7
8
9
10
 //運算表達式 Arithmetic expression
1+2*3 ; //回傳解果為7
//邏輯表達式 Logical Expression
true && (something === null || myAge < 18) //回傳結果為 true 或 false

//函式(或方法的呼叫) Function / Method Invocation Expression
Math.pow(2,10) //回傳結果為 1024

//三元運算子 Ternary Operator
myAge < 18 ? 'Youngster' : 'Adult'; //回傳結果為 'Youngster' 因為 myAge < 18

而敘述式:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(/*expression 1*/){
//若 expression1 為 true 則執行

}else if(/*expression 2*/){
//若 expression2 為 true 則執行
} else{
//若 expression1、expression2 都不為 true 則執行此
}

//迴圈敘述式 Looping Statement
while (/*expression */){
//若 expression 為 true, 則重複執行直到 expression 為 false 時跳脫
}
  • 按照上面敘述式的定義 — 由於敘述式是在敘述運行流程,而不會回傳值,所以你才不會在 JavaScript 裡面看到這樣的寫法:
1
2
3
4
5
6
// JS 並有提供這寫法
const status = if(Age <18 ){
return 'Young'
}else{
return 'Adult'
}
  • 注意:敘述式不一定是多行式(或區塊式)地呈現的最佳案例:變數宣告的指派敘述式(Variable Declaration Assignment Statement)
    1
    2
    //變數宣告的 指派敘述式
    const foo =123;
  • 請問上面指派式會回傳什麼結果 =>數字 123 或 udefined。正解為 udefined
    • 指派式的回傳結果為 undefined
  • 所以可以歸納出,javaScript 的變數宣告的指派式屬於敘述式,非表達式。

表達式什麼時候會以非單行的程式碼,也就是區塊的方式?

  • 立即呼叫函式表達式(IIFE)
1
2
3
4
const status = (funcrion(myAge){
if (myAge <18){ return 'Young'}
return 'Adult'
})(16) //假設填入參數為16,呼叫該函式的結果回傳 'Young'
  • 當可以分清楚表達式與敘述式差別後,回歸型別斷言
  • 斷言的基礎語法
    • 斷言的語法只能用在表達式上,因為表達式具備回傳值,敘述式則沒有
    • 因此,可以「斷言該表達式所運算結果之代表型別」

寫法如下:

1
2
3
<expression> as T assertion

<T assertion >(<expression>)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//運算表達式
(foo+bar*baz) as number;

// 邏輯表達式
(isPositive(num) && idEven(num)) as boolean;

//function
Math.pow(2, 10) as number

//
(myAge <18 ? 'Young' : 'Adult') as string;

//IIFE
(funcrion(myAge){
if (myAge <18){ return 'Young'}
return 'Adult'
})(16) as number

//只要是表達式,就算在其他敘述式、表達式內也能使用
somefunction(foo as number , bar as string) as boolean

換種寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<number>(foo+bar*baz);

// 邏輯表達式
<boolean> (isPositive(num) && idEven(num)) ;

//function
<number> Math.pow(2, 10) ;

//
<string>(myAge <18 ? 'Young' : 'Adult') ;

//IIFE
<number>(
funcrion(myAge){
if (myAge <18){ return 'Young'}
return 'Adult'
})(16)
);
//只要是表達式,就算在其他敘述式、表達式內也能使用
<boolean>somefunction(foo as number , bar as string)

以上針對註記 Annotation V.S. 斷言 Assertion 做個簡單整理~

最近遇到將表單資料存到後端的方式,過去一直以為是將資料整理為物件方式存取即可,後來發現使用FormData來建立物件傳送檔案即可!

  • FormData 是一個表單格式,利用它將資料轉成表單的格式,並且以表單的形式回傳給後端

建立 FormData 物件

  • 最直接方式,先取得form的資料
1
2
3
4
5
6
7
8
// 根據表單元素取得表單資料
var form = document.querySelector('form');
var formData = new FormData(form);

// 可以用 append 把欄位灌進去表單中
var formData = new FormData(); // 一開始表單的資料是空的
formData.append('username', Chris); // 增加欄位
formData.append('image', file); // 檔案也可以透過 append 放進來

以下介紹幾個基礎的使用方式:

  • formData.append(name, value) : 加入有 name 和 value 的表單資料(name:表單標籤的名稱,value: input 輸入的值)
  • formData.append(name, blob, fileName) : 加入字段,就像它是 <input type="file">,第三個參數 fileName 設置檔案名
  • formData.delete(name) : 移除帶有 name 的字段,
  • formData.get(name) : 取得帶有 name 的值,
  • formData.has(name) : 如果帶有給定 name 的字段,返回 true,否則返回 false。

使用axios 傳遞資料

1
2
3
4
5
6
7
8
9
var formData = new FormData();
//宣告要取得的值
var title = form.name
formData.append("formTitle", title);
axios.post('upload_file', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})

補充 vue+Typescript 宣告型別

  • 以 stack overflow 內的資料為例:
    • 可以使用 ref來取得DOM元素,並宣告該變數為HTMLFormElement
      1
      2
      3
      4
      5
      6
      <el-form
      class="m-auto"
      ref="formRef"
      :model="form"
      :rules="rules"
      >
      1
      const formRef = ref<HTMLFormElement>();
  • 另是使用 element-UI 的表單,也可以使用
    import { ElForm } from "element-ui";

如何使用jQuery AJAX submit 傳送form表單方法
JavaScript / jQuery 前端開發入門實戰
JavaScript Info-formData
stackoverflow

useAsyncData

1
2
3
4
5
6
7
8
9
10
const {
data: Ref<DataT>,
pending: Ref<boolean>,
refresh: (force?: boolean) => Promise<void>,
error?: any
} = useAsyncData(
key: string,
fn: () => Object,
options?: { lazy: boolean, server: boolean }
)
  • 頁面、組件中都可以使用useAsyncData 取得資料
  • Key :寫一個唯一的id
    請求資料的函式
  • 使用 useAsyncData 會回傳物件
    data : 返回的資料
    pedding : 指出資料是否被取得
    refresh : 是否要刷新資料

    Option 介紹

  • Lazy 延遲載入,直到數據獲取時,才顯示於頁面
  • 默認會是 false
  • Server 頁面是在server端要載入,就可以設定為true
  • Pick 從回傳的資料中,選擇要使用的key值

useAsyncData撰寫方式的範例

  • 本地建立資料夾
    • server/api/count.ts
1
2
3
4
5
let counter = 0
export default () => {
counter++
return JSON.stringify(counter)
}
  • app.vue,使用 useAsyncData
1
2
3
4
5
6
7
<script setup>
const { data } = await useAsyncData('count', () => $fetch('/api/count'))
</script>
<template>
Page visits: {{ data }}
</template>

useFetch

  • 頁面、組件中可以使用useFetch獲取任意URL資源
  • useFetch包裝 了useAsyncData 和 $fetch
  • 會根據 URL 自動生成 Key,也會去推斷API 的類型a
    1
    2
    3
    4
    5
    6
    const {
    data: Ref<DataT>,
    pending: Ref<boolean>,
    refresh: (force?: boolean) => Promise<void>,
    error?: any
    } = useFetch(url: string, options?)
1
2
3
4
5
6
7
<script setup>
const { data } = await useFetch('/api/count')
</script>

<template>
Page visits: {{ data.count }}
</template>

補充:其他2個類似方式

  • useLazyAsyncData : 方法等於useAsyncData,僅設置了lazy選項為true
  • useLazyFetch : 方法等於useFetch,僅設置了lazy選項為true

實作範例:

  • /api/mountains/everest
    • API 會產生的物件資料
1
2
3
4
5
6
7
8
9
10
11
{
"title": "Mount Everest",
"description": "Mount Everest is Earth's highest mountain above sea level, located in the Mahalangur Himal sub-range of the Himalayas. The China–Nepal border runs across its summit point",
"height": "8,848 m",
"countries": [
"China",
"Nepal"
],
"continent": "Asia",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Everest_kalapatthar.jpg/600px-Everest_kalapatthar.jpg"
}

Nuxt 3 為了提高效率會在取得資料後,緩存在當前頁面的payload
官方建議:如果只打算在組件中使用到 title 和 description ,你可以使用關鍵的屬性運用即可
選項中加入 pick,裡面的值就是取得資料中的key值

1
2
3
4
5
6
7
8
<script setup>
const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['title', 'description'] })
</script>

<template>
<h1>{{ mountain.title }}</h1>
<p>{{ mountain.description }}</p>
</template>

參考資料

要熟悉 TypeScript 的型別訂定,並不容易,而且撰寫時也會花費較多的時間,但好處是減少程式碼的錯誤,也讓協作者更容易解讀對方撰寫的程式碼。
萬事起頭難,不同的地方就是多讀幾次,或是尋找相關的範例來相呼應!

Class

傳統方法中,JavaScript 透過建構函式實現類別的概念,透過原型鏈實現繼承。而在 ES6 中,我們終於迎來了 class。

  • 定義一切事務的抽象特點

  • object :class 的實例 new Class 生成

  • 物件導向 OOP(Object Oriented Programming) :三大特性 封裝、繼承、多型

    • 封裝(Encapsulation):將對資料的操作細節隱藏起來,只暴露對外的介面。外界呼叫端不需要(也不可能)知道細節,就能透過對外提供的介面來訪問該物件,同時也保證了外界無法任意更改物件內部的資料
    • 繼承(Inheritance):子類別繼承父類別,子類別除了擁有父類別的所有特性外,還有一些更具體的特性
    • 多型(Polymorphism):由繼承而產生了相關的不同的類別,對同一個方法可以有不同的響應

      複習下 ES6 撰寫方式

  • 使用 class 定義類別,使用 constructor 定義建構函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal{
constructor(name){
this.name = name;
}
run () {
return `${this.name} is running`
}
}
const snake = new Animal('lily')
console.log(snake.run()); // lily is running

// 繼承的特性(繼承父類的屬性及方法), 繼承 run 的方法
class Dog extends Animal {
bark() {
return `${this.name} is barking`
}
}

const bao = new Dog('bao')
console.log(bao.run()) // bao is running
console.log(bao.bark()) // bao is barking
  • 使用 extends 關鍵字實現繼承
  • 这里我们重寫構造函式,注意在子類的構造函式中,必須使用 super 調用父類的方法,否則會報錯。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 
class Cat extends Animal {
constructor(name) {
super(name)
console.log(this.name) //maomao
}
run() {
// run 方法重寫
return 'Meow, ' + super.run()
}
}
const maomao = new Cat('maomao')
console.log(maomao.run()) //Meow, maomao is runing

TypeScript 中的 class

TypeScript 可以使用三種訪問修飾符(Access Modifiers)

  • public 修飾的屬性或方法是公有的,可以在任何地方被調用到,預設所有的屬性和方法都是 public 的
  • private 修飾的屬性或方法是私有的,不能在声明它的类的外部調用
  • protected 修飾的屬性或方法是受保护的,它和 private 類似,區别是它在子類中也是允许被訪問的
    例子說明:
  • name 被設定為了 public,所以直接訪問實例的 name 屬性是允許的。
1
2
3
4
5
6
7
8
9
10
11
class Animal {
public name;
public constructor(name) {
this.name = name;
}
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
console.log(a.name); // Tom
  • 希望有的屬性是無法直接存取的,這時候就可以用 private 了:
  • name 設為 private
1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal {
private name;
public constructor(name) {
this.name = name;
}
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
  • 使用 private 修飾的屬性或方法,在子類別中也是不允許訪問的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
private name;
public constructor(name) {
this.name = name;
}
}

class Cat extends Animal {
constructor(name) {
super(name);
console.log(this.name);
}
}

// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
  • 而如果是用 protected 修飾,則允許在子類別中訪問:
  • 將上述程式碼修改一下
1
2
3
4
5
6
class Animal {
protected name;
public constructor(name) {
this.name = name;
}
}

類別 class 的型別

給類別加上 TypeScript 的型別很簡單,與介面類似:

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi(): string {
return `My name is ${this.name}`;
}
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

類別

class 與 interface

  • 介面(Interfaces):不同類別之間公有的屬性或方法,可以抽象成一個介面。介面可以被類別實現(implements)。一個類別只能繼承自另一個類別,但是可以實現多個介面
  • 在 物件導向 世界中,一個 class 只能繼承自另外一個class
  • 有時候不同 class 之前,可以有共同的特性,使用子類繼承父類的方法很難來完成
  • class 可以使用 implements 來實現 interface (提高 OOP 的靈活性)

範例一:

  • 有 車子和手機,都有打開radio的功能
  • 可以將兩個共有的抽取為一個 interface
1
2
3
4
5
6
7
8
9
10
11
class Car  {
switchRadio(trigger: boolean) {

}
}

class Cellphone {
switchRadio(trigger: boolean) {

}
}

可以將兩個共有的抽取為一個 interface

  • void => 代表什麼都不返回
  • 在class 後面放入 implements 讓類去實現它
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Radio {
switchRadio(trigger: boolean): void;
}

class Car implements Radio {
switchRadio(trigger) {
return 123
}
}
class Cellphone implements Radio {
switchRadio() {
}
}

範例二

  • 新特性為 檢查電池的容量
  • 是手機有,但汽車沒有的
  • 所以新建立一個 interface (檢查battery)
    • 此功能只有手機有,所以放在手機的class
1
2
3
4
5
6
7
8
9
10
11
interface Battery {
checkBatteryStatus(): void;
}
// 要實現多个介面,我们只需要中間用 都好 隔开即可。
class Cellphone implements Radio, Battery {
switchRadio() {
}
checkBatteryStatus() {

}
}

此外 interface 之間有繼承關係

  • 建立 radioWithBattery 繼承 Radio ,裡面再放入檢查電量的設定
1
2
3
4
5
6
7
8
9
10
11
12
interface radioWithBattery extends Radio {
checkBatteryStatus(): void;
}


class Cellphone implements radioWithBattery {
switchRadio() {
}
checkBatteryStatus() {

}
}

類別與介面

enum 列舉

  • 常數指執行程序中不會被改變的值,在 JS 中我們一般會用 const 來宣告
  • 但有些取值是在一定範圍內的一系列常數。
    • 如:一周內七天、三原色(紅、黃、藍)、方向(上、下、左、右)

      範例一

  • 數字列舉
  • 列舉成員會被賦值為從0,開始遞增
1
2
3
4
5
6
7
8
9
10
11
// 數字列舉,一个數字列舉可以用 enum 这个關鍵詞來定義,我们定義一系列的方向,然后这里面的值,列舉成员会被赋值为從 0 开始遞增的數字,
enum Direction {
Up,
Down,
Left,
Right,
}
console.log(Direction.Up) //0
// 還有一个神奇的點是這個列舉还做了反向映射
console.log(Direction[0]) //up

  • 可以手動賦予值
  • 未手動賦值的列舉項會接著上一個列舉項遞增。
1
2
3
4
5
6
7
8
9
10

Up = 10,
Down,
Left,
Right,
}
console.log(Direction.Down) // 11 ,後面的項目數值會遞增

console.log(Direction[0]) //up

範例二

1
2
3
4
5
6
7
8
9
10
11
// 字符串列舉
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const value = 'UP'
if (value === Direction.Up) {
console.log('go up!')
}

範例三

  • 常數列舉
    • 編譯後的邏輯變少了
    • 使用常數列舉會內連列舉的用法並且不會將設定的列舉編譯成 JS 程式碼
1
2
3
4
5
6
7
8
9
10
const  enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const value = 'UP'
if (value === Direction.Up) {
console.log('go up!')
}


泛型 Generics

  • 要解決什麼問題
    • 建立函式 echo 參數為 arg ,接著宣告變數傳入參數
    • 但其中的型別沒有設定
    • 傳入和返回的型別無法統一
1
2
3
4
5
function echo(arg) {
return arg
}
const result = echo(123)
// 這時候我們發現了一個問題,我们傳入了數字,但是返回了 any

  • 當在建構 function、internet及Class 時,你會希望這些component都能被重複運用的 Generic(泛型)提供了一個彈性的作法。
  • 語法是: <T>

範例一

  • 泛型(Generics)是指在定義function、internet及Class的时候,不预先指定具體的型別,而在使用的时候再指定型別的一種特性。
1
2
3
4
5
function echo<T>(arg: T): T {
return arg
}

const result = echo(123)

範例二 傳入多個

1
2
3
4
5
6
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}

const result = swap(['string', 123])

泛型第二部分 - 约束泛型

  • 帶有「限制」的泛型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function echoWithArr<T>(arg: T): T {
console.log(arg.length)
return arg
}

// 上例中,泛型 T 不一定包含属性 length,我们可以给他傳入任意型別,當然有些不包括 length 属性,那样就會報錯

// 我們使用了 extends 約束了泛型 T 必須符合介面 IWithLength 的形狀,也就是必須包含 length 屬性。
interface IWithLength {
length: number;
}
function echoWithLength<T extends IWithLength>(arg: T): T {
console.log(arg.length)
return arg
}

echoWithLength('str')
const result3 = echoWithLength({length: 10})
const result4 = echoWithLength([1, 2, 3])

//此時如果呼叫 loggingIdentity 的時候,傳入的 arg 不包含 length,那麼在編譯階段就會報錯了
echoWithLength(7);
// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'


泛型第三部分 - 泛型在 class 和 interface 中的使用

  1. 泛型在 class 的使用
  • 在第一個程式中存在一个问题,它允許你向 Queue 中添加任何型別的數據,當然,當數據被彈出隊列时,也可以是任意類型
  • 在下方的範例中,看起来可以向隊列中添加 string 型別的數據,但是那麼在使用的過程中,就會出現無法捕捉的錯誤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Queue {
private data = [];
push(item) {
return this.data.push(item)
}
pop() {
return this.data.shift()
}
}

const queue = new Queue()
queue.push(1)
queue.push('str')
console.log(queue.pop().toFixed())
console.log(queue.pop().toFixed())


1
2
3
4
5
6
7
8
9
10
11
12
class Queue<T> {
private data = [];
push(item: T) {
return this.data.push(item)
}
pop(): T {
return this.data.shift()
}
}
const queue = new Queue<number>()


  1. 泛型在 interface 中的使用
  • 之前提過可以使用介面的方式來定義一個函式需要符合的形狀
  • 當然也可以使用含有泛型的介面來定義函式的形狀:
1
2
3
4
5
6
7
8
9
10
11
//泛型和 interface
interface KeyPair<T, U> {
key: T;
value: U;
}

let kp1: KeyPair<number, string> = { key: 1, value: "str"}
let kp2: KeyPair<string, number> = { key: "str", value: 123}

let arr:number[] = [1,2,3];
let arrTwo:Array<number> = [1,2,3]

interface 搭配泛型之后,可以靈活的返回不同的型別

  • 創建一个拥有特定型別的容器,class 和 泛型 仿佛给一个容器貼上標籤一样

  • 泛型就好像一个可變的參數,在用的时候傳入,生成这個不同型別的一个容器,

  • 上個部分的用它来靈活的约束参数的型別,不需要參數是一个特别死板的型別,不希望他是一个特定 string、number 型別,我要傳入的参數必须有某某屬性、某某方法,否則就會報錯。

  • 在函式使用的时候,函式的这个型別推斷,不會進入到函式中,所以使用表達式,没法明確建立型別的绑定,用泛型可以让我们打破這個鴻溝,這個时候就可以返回它傳入的類型。

    Type Aliases

  • 就是给型別起一個别名,讓它可以更方便的被重用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let sum: (x: number, y: number) => number
const result = sum(1,2)
type PlusType = (x: number, y: number) => number
let sum2: PlusType

// 支持聯合
type StrOrNumber = string | number
let result2: StrOrNumber = '123'
result2 = 123 //沒有錯

// 字符串字面量 ,類似在列舉的常數變量
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
// 使toWhere 這個變量為 Direction 類型
let toWhere: Directions = 'Up'

  • 我們使用 type 定了一個字串字面量型別 EventNames,它只能取三種字串中的一種。
    注意,型別別名與字串字面量型別都是使用 type 進行定義。
1
2
3
4
5
6
7
8
9
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}

handleEvent(document.getElementById('hello'), 'scroll'); // 沒問題
handleEvent(document.getElementById('world'), 'dbclick'); // 報錯,event 不能為 'dbclick'

// index.ts(7,47): error TS2345: Argument of type '"dbclick"' is not assignable to parameter of type 'EventNames'.

Intersection Types

  • 使用 & 符號
  • 經過上面的 type 使 IName 就有了 name 和 age兩個屬性在裡面
    • 和interface 的 extends 有點類似,都是為了實現物件形狀組合和擴展
1
2
3
4
5
6
interface IName  {
name: string
}
type IPerson = IName & { age: number }
// 經過上面的 type 使 IName 就有了 name 和 age兩個屬性在裡面
let person: IPerson = { name: 'hello', age: 12}

什麼时候用介面,什麼时候用 Type Aliases:

  • interface 是 docker typing 的實現方式,是一種獨特類型,和extends class implememts 有關的用interface ,
  • 和交叉,聯合型別的有關的用 Type Aliases;

內建物件

DOM 和 BOM 的內建物件

DOM 和 BOM 提供的內建物件有:
Document、HTMLElement、Event、NodeList 等。
TypeScript 中會經常用到這些型別:

1
2
3
4
5
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});

ECMAScript 的內建物件

  • Boolean、Error、Date、RegExp 等。
1
2
3
4
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

TypeScript 內置

Utility Types

官方
Typescript 還提供了一些功能性,帮助性的型別,這些型別,大家在 JS 的世界是看不到的,這些型別叫做 utility types,提供一些簡潔明快而且非常方便的功能。

  • 使用 Partial
  • Omit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// partial,它可以把傳入的型別都變成可選
interface IPerson {
name: string
age: number
}

let viking: IPerson = { name: 'viking', age: 20 }
type IPartial = Partial<IPerson>
let viking2: IPartial = { } //可以都不傳,也不會報錯

// Omit,它返回的型別可以忽略傳入型別的某个屬性
// 以下範例 將 name 忽略掉
type IOmit = Omit<IPerson, 'name'>
let viking3: IOmit = { age: 20 }

資料來源:

  1. 線上課程:實戰 Vue3.0(正式版) + TS
  2. TypeScript 新手指南

因為工作需求,有需多缺乏的技能需要補齊,TypeScript 就是其中一項。
上班看不懂的時候邊查詢,空閒時間再來上課不足不懂的知識點,但還是得透過文章的整理來好好消化一下!
既然要開始介紹 TypeScript 就先來說說使用它的原因吧。

為什麼使用 TypeScript

  • 程式語言中,有區分強型別和弱型別兩種類型。

  • 而 JavaScript 是弱型別語言。

  • 在 JavaScript 中,他會自己判斷應該執行的型別。自行做[型別自動轉換]

  • 在快速開發時,很有可能引發一些預期之外的錯誤,相當不利於多人協同開發。

    • 優點:寫法彈性。
    • 缺點:無法在開發時期檢查型別。
  • 依據上述的問題,所以使用 TypeScript

  1. 使程式碼更容易理解 (如函式需傳入的參數可以明確設定型別)
  2. 減少程式碼撰寫的錯誤
  3. 可以完全兼容JavaScript,此外能與 ES6 完美結合,並提供更多支援寫法。

安裝介紹

  • 就是直接到官方網站查看,依需求安裝

npm install -g typescript

  • 檢查版本
    tsc -v

型別介紹

從 JavaScript 型別分類開始

字串 string

  • 和ES6一樣,可以使用[字串樣板 Template Literals]
    1
    2
    3
    let firstName:string = 'NaNa'
    let message: string = `Hello,${firstName}`

    布林值 boolean

布林值是最基礎的資料型別,在 TypeScript 中,使用 boolean 定義布林值型別:

1
let isDonw:boolean = false 

注意:使用建構函式 Boolean 建立的物件不是布林值:

  • 事實上 new Boolean() 返回的是一個 Boolean 物件
    1
    2
    3
    4
    let createdByNewBoolean: boolean = new Boolean(1);

    // Type 'Boolean' is not assignable to type 'boolean'.
    // 'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.
    1
    在 TypeScript 中,boolean 是 JavaScript 中的基本型別,而 Boolean 是 JavaScript 中的建構函式。

數值 number

1
2
3
let age:number = 10
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

Null 和 Undefined

  • 與 void 的區別是,undefined 和 null 是所有型別的子型別
    • unfefined 可以賦值給number類型的變數
    • 陣列被赋值為 undefined 或 null 不会报错
    • let num:number = undefined
1
2
3
4
5
6
let u: undefined = undefined;
let n: null = null;

// 這樣也不會報錯
let u: undefined;
let num: number = u;

void

  • 通常用在當函式沒有回傳值時。
  • 而 void 型別的變數不能賦值給 number 型別的變數:
    1
    2
    3
    4
    let u: void;
    let num: number = u;

    // Type 'void' is not assignable to type 'number'.

    any

  • 在任意值上任何属性都是允許的
    • 當有明確型別時,應避免使用 any
    • 因為 any 可以任意調用方法和屬性,很有可能出現錯誤(就喪失類型檢查的意義)
      如果是 any 型別,則允許被賦值為任意型別
1
2
let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

未宣告型別的變數

變數如果在宣告的時候,未指定其型別,那麼它會被識別為任意值型別:

1
2
3
4
5
let something;
something = 'seven';
something = 7;

something.setName('Tom');

陣列 Array 與 元組 Tuple

陣列

  1. 在想宣告的型別後面加上[],宣告為某個型別的Array
    1
    let idList:number[] = [1,2,3]
  2. 使用陣列泛型,Array <元素型別>
    1
    let list:Array<number> = [1,2,3]
  • 注意,當已經宣告這是個數字陣列
    • 要 push 字串進去,會出現錯誤
      1
      2
      let list:Array<number> = [1,2,3]
      list.push("4")

      元組 Tuple

    • 剛剛的陣列將同一類型的數據放在一起,但是,想加入不同型別的數據怎麼辦?
    • 表示方式與陣列相似,將型別寫在[]
    • 有固定長度和元素型別的陣列
      1
      2
      3
      4
      // 表示方式和陣列非常相似,只不过它将型別寫在了里面 這時會對每一項產生了限定的作用
      let user: [string, number] = ['viking', 20]
      //但是当我们少寫一項 就會報錯,同樣多寫也會報錯
      user = ['molly', 20, true]

元組

物件的型別——介面 (Interface)

  • Interface 可以用來定義物件,還有由物件所延伸的型別(例如,陣列、函式)
  • age?: number; 在該屬性後面加上 ? 表示為可選屬性 ,也就是在宣告新的物件時,可以彈性加入或不加入age (也不會報錯)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 我们定義了一個介面 Person
interface Person {
name: string;
age: number;
}
// 接着定义了一个變數 NaNa,它的型別是 Person。
//这样,我们就约束了 NaNa 的形狀必須和介面 Person 一致。
let NaNa: Person ={
name: 'NaNa',
age: 20
}

//有时我们希望不要完全匹配一个形狀,那么可以用可選屬性:
interface Person {
name: string;
age?: number;
}
let NaNa: Person = {
name: 'NaNa'
}


唯讀屬性

  • readonly 是用在屬性上面
  • 希望物件中的一些欄位只能在建立的時候被賦值,那麼可以用 readonly 定義唯讀屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
readonly id: number;
name: string;
age?: number;
}
// 建立物件
let Vic: Person ={
id:1,
name: 'Vic',
age: 20
}
//不能去修改id
Vic.id = 9527

函式型別

  • 函式可以作為參數、可以存入陣列,可以被另外一個函式返回、可以被賦值另外一個變數
  • 函式是由兩部分構成,一個是輸入(通過不同參數來實現),二為輸出(就是函數的返回結果)

    例子一

  • 設定參數型別、返回的型別
  • 若參數設定了 x、y兩個,也就只能放入兩個
    1
    2
    3
    4
    // 来到我们的第一个例子,约定输入,约定输出
    function add(x: number, y: number): number {
    return x + y
    }

例子二

  • 也可以設定可選參數
  • 在 z? 表示 z 可有可無
1
2
3
4
5
6
7
8
// 可选参数
function add(x: number, y: number, z?: number): number {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
  • 而在可選參數後面不可以再添加確定參數

例外說明:引數預設值

  • 在 ES6 中,我們允許給函式的引數新增預設值,TypeScript 會將添加了預設值的引數識別為可選引數:
1
2
3
4
5
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom'); // 因為 lastName 添加了預設值,識別為可選引數

此時就不受「可選引數必須接在必需引數後面」的限制了:

1
2
3
4
5
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

例子三

  • 函式的表達式
1
2
3
4
5
6
7
8
9
10
11
const add = (x: number, y: number, z?: number): number => {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}

// 函式本身的类型
const add2: (x: number, y: number, z?:number) => number = add

  • 函式不只輸入、輸出有類型,本身也有類型
  • 將 add2 賦予 string 會出錯誤
  • 所以須聲明一模一樣的 const add2: (x: number, y: number, z?:number) => number = add

例子四

  • 使用 interface (單純定義函式的 Interface)
1
2
3
4
5
6
7
8
// interface 描述函数类型
const sum = (x: number, y: number) => {
return x + y
}
interface ISum {
(x: number, y: number): number
}
const sum2: ISum = sum
  • 另一個範例
1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}

型別推論(Type Inference)\ 聯合型別(Union Types) \ 型別斷言(Type Assertion)

型別推論(Type Inference)

  • 可以推論出我們賦值過程中,這個變數應該是什麼類型的
  • TS 會在沒有明確指定型別的時候,推測出型別

聯合型別(Union Types)

  • 用分隔符號
  • 當 TypeScript 不确定一个聯合型別的變數到底是哪個型別的时候,我们只能使用此聯合型別的所有型別裡共有的屬性或方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 允許 numberOrString 的型別是 string 或者 number,但是不能是其他型別。
let numberOrString: number | string
// 而在使用此聯合型別的所有型別里共有的屬性性或方法:
numberOrString.length //會報錯 (只有 string 適用)
numberOrString.toString() //string 和 number 的共同屬性是沒問題

//若放在函式定義型別
function getLength(something: string | number): number {
return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.

  • 聯合型別的變數在被賦值的時候,會根據型別推論的規則推斷出一個型別:
1
2
3
4
5
6
let numberOrString: string | number;
numberOrString = 'seven';
console.log(numberOrString.length); // 5
numberOrString = 7;
console.log(numberOrString.length); // 編譯時報錯
// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

上例中,第二行的 numberOrString 被推斷成了 string,存取它的 length 屬性不會報錯。
而第四行的 numberOrString 被推斷成了 number,存取它的 length 屬性時就報錯了。

  • type guard
  • 當遇到聯合類型,可以使用條件語句,自動幫你縮小型別範圍
1
2
3
4
5
6
7
8
// typescript 在不同的条件分支里面,智能的缩小了范围,这样我们代码出错的几率就大大的降低了。
function getLength2(input: string | number): number {
if (typeof input === 'string') {
return input.length
} else {
return input.toString().length
}
}

型別斷言(Type Assertion)

  • 開發者比 TS 更了解編寫的程式碼。因此,TS 允許開發者覆蓋它的推論,這樣的機制稱為「型別斷言」。
1
2
3
4
5
6
7
8
9
10
// 这里我们可以用 as 關鍵字,告诉typescript 这里我把它看作是一個 string,你可以给他用 string 的方法。
function getLength(input: string | number): number {
const str = input as string
if (str.length) {
return str.length
} else {
const number = input as number
return number.toString().length
}
}
  • 型別斷言不是型別轉換,斷言成一個聯合型別中不存在的型別是不允許的:
1
2
3
4
5
6
7
function toBoolean(something: string | number): boolean {
return <boolean>something;
}

// index.ts(2,10): error TS2352: Type 'string | number' cannot be converted to type 'boolean'.
// Type 'number' is not comparable to type 'boolean'.

基礎型別先介紹到這裡,下篇將介紹一些進階用法~

資料來源:

  1. 線上課程:實戰 Vue3.0(正式版) + TS
  2. TypeScript 新手指南