[學習之路] TypeScript 的基礎
如名般的 Type Script(類型腳本語言),微軟所提供的一種超集 JavaScript 之程式語言,可當作具有 type 類型系統的 JavaScript。主要是解決 JavaScript 的動態 type 設計不良所存在,可以當做它是一種 JS 預處理前置作業的檢查類型無誤後透過編譯 complier 後轉為 JavaScript。TypeScript 的語法可以使用純 JavaScript 來編寫,兩者差異就只是 type 的補足完整宣告。
由於 JavaScript 原本誕生作為簡單的腳本語言,隨著主流性發展導致原本語意單調設計上的引起錯亂,舉例來說:
console.log("" == 0, 1 < 5 < 3, 5 * undefined); |
因此修正 JS 的語意類型的前置作業就是 TypeScript 的存在目標。TypeScript 的存在來自於 JavaScript 的問題性改善:
- 靜態類型 Static Type 檢查
由於 JavaScript 採用動態方式特別自由不受限制,容易發生預期以外之型態結果(如上面範例);TypeScript 採用靜態類型模式,在使用變數時就必需指定類型做為指定並檢查。 - 變數領域規劃
JavaScript 的變數只能作為全域變數或區間變數使用,無法在某物件或類別 class 內使用。 - 物件導向邏輯不同
JavaScript 的物件導向觀念採用獨特的原生鍊 Prototype Based 類型並非屬於程式領域中正規的 Class Based 之觀念(但在 ES6 版本已出現)。因此很多程式設計者無法套用原本已熟悉的物件導向觀念做功能使用;TypeScript 則可使用 Class Based 觀念並加以使用,像是 classs 繼承與介面等正規物件導向功能,更適合大型專案開發所用。
安裝
可從 官方網站 深入了解並下載,這裡使用 npm 來獲得安裝 (Node.js)。
npm install typescript -g |
這裡需引數 g 來安裝到主機上而不是專案目錄下
執行
試著在專案內新增一筆檔案 test.ts
,跟著輸入以下代碼準備轉檔
class loki { |
直接使用 tsc 指令
直接單純的使用 TSC 模組來完成指定檔案轉換,後透過終端機指令 tsc test.tsc
會進行編譯成 test.js
。另外提供直接用 ES6 方式寫的差別做比對:
var loki = /** @class */ (function () { |
class loki { |
從上列可知道以下觀念(觀察 TypeScript 與 JavaScriptES6):
- 編寫 class 的程式觀念上,使用 TypeScript 更直覺操作。畢竟原 ES5 寫法不是標準的 class 而是採用函式。雖 ES6 已經支援 Class 了,這裡因 TypeScript 的預設引數為 ES5 語法,但可自行調整此編譯語法引數。
- TypeScript 主要是協助開發者完成代碼後轉換成 JavaScript 能理解的寫法
- TypeScript 對於變數的型態宣告有強迫性,這是為了幫助開發者風險降低
- TypeScript 的主要用途是讓開發者寫得更簡短與穩定,不是為了取代 JavsScript 標準
事實上,任何轉檔調整需求都需要專案根目錄下的 TypeScript 設定檔案tsconfig.json
來控制轉檔細部設定。這裡沒提供則不影響皆依 tsc 預設為輸出。在此試著規劃引數檔。
- 專案目錄下直接建立
tsconfig.json
檔案。可以手動自己建立,也能透過終端指令tsc --init
完成並會提供引數說明。 tsconfig.json
的引數非常多,這裡只隨便兩種設定即可,未設定的都以初始值,甚至都不寫也可以。{
"compilerOptions": {
"target": "es6", //輸出 JS 版本,可選擇 ES3、 ES5 、 ES2015 、 ES2016 、 ES2017 、 ES2018 、 ESNext 和 JSON
"module": "ES6", //指定生成為哪種模組,可不寫則根據 target 為 ES3/ES5 預設為 commonjs,否則預設值為 ES6
}
}
//更多引數詳閱 https://www.staging-typescript.org/tsconfig- 接著在終端指令輸入
tsc
就能根據 tsconfig.json 來轉檔,預設會自動對該專案資料夾內所有 ts 或 tsx 檔案為對象。
由 VSCode 提交 tsc 指令
而 VScode 本身除了支援 TypeScript 語法高量與智能提示,還能進一步透過 tasks 工作排程方式送出 tsc 指令。前提是專案內要存在tsconfig.json
才能被 VS Code 協助(必要)。
- 接著換個方式。改用 VSCode 的 task 功能來執行 tsc。按下F1來呼叫命令視窗,輸入 task 關鍵字選擇
Task:Configure Task
(工作:設定工作)。 - 此時因為專案下有 tsconfig.json 檔案,VSCode 會對此檔案產生
建置 build
(一次性轉換)與監看 watch
(同步轉換)兩種工作指令。
建置 build
- 選擇
build 建置
則會再產生.vscode/tasks.json
記住此工作排程設定。內容如下:.vscode/tasks.json {
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
],
"group": "build",
"label": "tsc: 建置 - tsconfig.json",.
}
]
} - 現在專案已經綁了一個 Task 工作細節,要執行工作的方式為按下CTRL+SHIFT+B選取工作即可,選擇剛建立的
tsc: 建置 - tsconfig.json
。 - 自動彈出終端機並自行執行 tsc -p 指令,同時要求你按下任何按鈕關閉此介面。
Executing task: tsc -p d:\github\test\tsconfig.json <
工作將被重新啟用。按任意鍵關閉。 - 如果覺得這個按鈕動作很討厭,根據官方手冊可以不特別彈出顯示。
.vscode/tasks.json {
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
],
"group": "build",
"label": "tsc: 建置 - tsconfig.json",
"presentation": {
"reveal": "silent" //僅當未掃描輸出以查找錯誤和警告時,才將終端面板置於前面。
}
}
]
}
監看 watch
- 回到上面流程這次改選
監看 watch
則會再對.vscode/tasks.json
產生一個新工作排程設定。內容如下:.vscode/tasks.json {
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
],
"group": "build",
"label": "tsc: 建置 - tsconfig.json",
"presentation": {
"reveal": "silent"
}
},
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": [
"$tsc-watch"
],
"group": "build",
"label": "tsc: 監看 - tsconfig.json"
}
]
} - 這次改執行這個工作項目,按下CTRL+SHIFT+B選取剛建立的
tsc: 監看 - tsconfig.json
。 - 現在每次對目標檔案 ts 存檔時,就會自動監看變化並同步轉檔。
tsconfig.json 引數
這裡列一些值得討論的引數設定(內容隨作者慢慢增加),不設定也沒關係。你可以跳過這篇等到上手環境再回來看。
compilerOptions | default Value | Allowed | info |
---|---|---|---|
target | ES3 | ES5, ES6/ES2015 (synonymous), ES7/ES2016, ES2017, ES2018, ES2019, ES2020, ESNext |
輸出的 JS 版本 |
noEmitOnError | false | true | 如果報告了任何錯誤時停止 JS 輸出 |
類型判斷方式
在 TypeScript 領域內,行如其名所有的東西都會有靜態 Type,可分為系統自動判斷與人為手動定義兩種。當你未做任何動作情況下系統會自動判斷可能的 Type 來自動列入除錯考量。但可以的話就自行定義畢竟也可能會出現推論錯誤可能。
由於 TypeScript 是 TypeScript 的超集 (superset),因此在 Typescript 內輸入純 JavaScript 語法也是可行的,只差於在 TypeScript 領域裡會保守的做任何類型檢查並報錯。類型檢查有以下機制:
根據推論
TypeScript 了解 Javascript 的語法因此直接使用 Javascript 來設定類型時 TypeScript 會試著去推理出變數的類型。如下例,第一行沒有告知類型,TypeScript 能推測出屬於字串,接著第二行會出現錯誤,TypeScript 已知道這變數為字串而不能改成數字。
let jsword = "hello World"; |
透過定義
可以自己告知 TypeScript 這變數屬於什麼類型,有三種基本寫法:
const str: string = "hello"; /*指定寫法:在 name 處指定類型*/ |
這習慣很容易培養,每次創立變數時記得宣告這變數的類型即可,舉例如下:
const |
當然可試著將所有的定義類型移除,TS 程式仍可以正常轉換為 Javascript 不會報錯,這是 TypeScript 透過推論協助出來的。但不要太過於依賴推論功能,如果可以還是想成習慣宣告類型,避免遇到不可預期的邏輯錯誤。
類型
JavaScript 的型別分為兩種:原始資料類型(Primitive data types)和物件類型(Object types)。
原始類型 Primitive Types
又稱呼資料類型 Data Types,目前已看過 number,string,boolean,any 這四種基本類型的出現,我們接著詳細介紹類型與規則。但注意不要跟 JavaScript 原生的內建物件搞混,舉例來說boolean vs new Boolean()
,或number vs new Number()
。
boolean, number, string
布林值可以指定類型為boolean
,另一種寫法為Boolean
代表的是 JS 原生的建構式勿搞混。
const bl: boolean = false; |
數字類型涵蓋了各種數值
const |
字串如此簡單,之後不再演示 new String() 之類了唷。
const |
void, null, undefined
void 為空值之意,如果是宣告在變數身上代表沒有內容,宣告在函式身上是代表此函式沒有回傳內容。可以指定 undefind 或 null 作為值。
const void1: void = undefined; |
或者直接指定該類型為 undefind 或 null。此外任何類型都可以存在 undefined 或 null,void 則不可以(就真的空值沒有存在意義)。
const |
any 通用類型
由於 TypeScript 非常要求靜態類型的宣告,因此有必要時可使用 any 通用類型,可以像 Javascript 那樣保留動態不受限定類型。然而如果宣告變數時沒指定 type 也會被 TypeScript 當作 any。
let anyVal: any = 123; //原為 number |
複合類型
選擇宣告類型時,可以透過符號|
來告知此宣告允許多種 type 格式。
let val: string | number = 123; |
物件類型 Object types
又稱呼用戶自訂類型 User Defined Types。在 TypeScript 中所有不是原始類型的都是物件類型的子類。例如有 class、介面、函式、內建物件、陣列、元組等,我們將在後續章節中詳細介紹:
JavaScript 內建物件
JavaScript 的內建物件總類眾多
標準物件
標準的內建物件類型寫法如下,這裡不全列出,只需注意與原始類型差異為大寫命名方式。
let b: Boolean = new Boolean(1); |
BOM 與 DOM 物件
BOM 與 DOM 本身的物件也有各自的類型,其名字注意大概為 HTMLElement、NodeList、MouseEvent 這些常用,可嘗試不寫改由系統來推論。
let body: HTMLElement = document.body; |
介面 interface
介面是一種約束行為的自訂資料類型,能要求必需含有哪些項目類型,打包成一個自訂的介面 type。常用於 object 與 class。
interface Objtype { |
通常開發者命名習慣上會用字首大寫或 I 開頭,這樣能醒目知道而是一個介面類型的名稱。
繼承 extends
介面也可以透過繼承觀念來得到父介面與子介面的關係。繼承觀念在 class 會再討論一次。
interface Fa { |
合併介面
當存在相同名稱的介面可視同合併成一個同名介面,但注意屬性與類型組合不可有衝突。而方法的組合衝突時等同於函式的多載效果。
interface User { |
物件 object
如果是宣告一個物件資料 JSON。定義之前一定要先使用介面並將類型都指定整理好。接著將這個介面名稱當作類型進行宣告給予變數,有些須注意:
- 這個變數 object 必需與介面擁有一樣名稱與數量,否則缺少就會報錯。
interface Objtype { |
可選、任意、唯獨屬性
- 透過屬性名稱後綴
?
可指定某屬性為非約束之可選屬性,可存在也可不存在 [propName: string]: any
代表任何名稱與任何的值類型,如果把 any 改成 string,會影響 id 報錯(因為這句會套用其他的屬性上)- 透過屬性名稱前綴
readonly
,一旦被 obj 套入類型後,obj 這個屬性會獲得不可修改之狀況。
interface user { |
注意的是[propName: string]
等於是指任何屬性,也約束在其他已寫出的屬性,注意使用場合避免衝突。
interface user { |
陣列 array
陣列的指定類型方式需要多一個[]
宣告在後餟,這樣的操作下會是陣列內所有值的類型都是同樣的,雖然不像 Javascript 自由彈性,但也是保護你的資料都是同樣 type 類型。
let ary:number[]=[123,456]; |
如果需要彈性的類型資料,可使用複合方式達到。
let ary: (string | number)[] = ['Loki', 'Jiang', 18]; |
搭配泛型
陣列泛型(Array Generic)的寫法是專用給陣列的一種宣告方式。透過 Array 這個 JS 原生建構式獲得類型,並且傳遞 number 作為泛型的替代。
const ary: Array<number> = [1, 2, 3]; |
ary:any[]
的解讀角度比較像是 ary 本身是 any type 且為陣列結構之類型,而ary:Array<any>
的解讀為 ary 從 JS 建構式 Array 獲得類型,並由泛型的指定其內容值替代為 any type。
搭配介面
也可以用介面來做成約束類型給予陣列,但以下用法太複雜很少用於這種形式的陣列上。
interface Istr { |
如果是用在陣列內的 value:Object 上就蠻適合的。
interface Staff { //設計介面 |
列舉 enum
enum 是一種特別的資料類型稱呼為列舉類型。能預先將一些固定的資料存入並自動提供索引 key,結果會是以物件方式保存且不可再事後添加。使用列舉類型可以獲得 key 與 value 相反對應的物件,透過以下代碼做檢視就能明白:
enum lokiStatus { scuess, warn, error } |
TypeScript 會自動將這些 value 做成 key 值,如果需要就能去尋找這些值的相關動作。
enum lokiStatus { scuess, warn, error } |
也可以去手動設定這些 index 值,舉例來說前後端的 status 對應可以用到。
enum apiStatus { |
避免同時存在手動索引值與自動索引值共存,TypeScript 會笨笨從 0 列項開始自動賦予索引值,可能會自動地給覆蓋掉。例如
[a=2,b,c,d]
的 c 會自動拿到 2,導致 a 手動的 2 被覆蓋。
也能指定字串作為索引 key,只是不會額外產生反向關聯 key 與 value。
enum msgStatus { |
常數列舉 const enum
enum 可以使用 const 來宣告,差別在於編譯完成後會刪除 object 但仍可正常於 TypeScript 上去列舉資料(實際因需求而存在)。可以觀察 TypeScript 產生的 JavaScript 精省到什麼程度減少內存效能。然而因為物件不存在,所以無法反向取值透過 index 數字去找到 value。
const enum constStatus { |
聲明列舉 declare enum
聲明用途的列舉,僅檢查用途,編譯時不會有存在 object 與任何換算結果。
declare enum constStatus { |
元組 tuple
元組是指一種更嚴謹的陣列,相別於前面的寫法,能強迫嚴格指定每個陣列位置的類型為何。
let ary1: [string, string, number, string]; |
搭配列舉與別名的範例
enum sex { man, woman }; //列舉類型物件 |
如果搭配資料庫規劃可做成這樣的陣列宣告,將 type tuple 當作一個別名指定給陣列。
enum sex { man, woman }; //列舉類型物件 |
函式 Function
除了變數需要指定類型,在函式的應用上也會用到定義函式,例如傳遞引數、回傳值以及函式型變數等場合。
傳遞與回傳
規則如變數一樣,在傳遞變數後綴指定類型,而回傳的資料也需要類型,寫在函式本體後綴指定類型。
function calc(price: number, tax: number): number { //回傳類型寫在函式本身後綴 |
傳遞:選配、預設、其餘
如果傳遞引數為選配,可透過?
來指定。
function calc(price: number, tax?: number): void { //回傳類型寫在函式本身後綴 |
注意選擇性引數的順序需要於必要引數之後,不可以持有
?
的引數比沒有持有的引數還早出現。
而預設引數與 JavaScript ES6 相同觀念使用。
function calc(price: number, tax: number = 5): void { //回傳類型寫在函式本身後綴 |
其餘引數 rest parameter 同 JavaScript 用法,使用...
來表示不確定數量的引數,因為是一種陣列結構所以需宣告陣列類型。
function sumArg(...ary: number[]): void { |
回傳:void、never
如果沒有要回傳變數時,需函式類型為void
來告知這個函式沒有回傳資料。
function calc(price: number, tax: number): void { //回傳類型寫在函式本身後綴 |
不回傳 never 與 void 很相近都是用在於不會回傳的函式,主要嚴格用在沒有結果的函式(無限迴圈或拋出錯誤),例如:
function errorMsg(message:string): never{ |
超載 overload
Overload 機制用於考量同一個函式下,多種方案用途其有不同類型的引數與回傳。透過宣告定義函式持有多種載入輸出的不同類型使用。
function setConvert(arg: [string, number]): number; //函式定義:傳入 tuple 傳出 number |
乍看之下拿掉函式定義單靠函式實現的複合類型也能正常執行,但其真正差異於當程式開始檢查型態時會從第一組函式定義循序做檢查。
函式表達式
前面介紹的都是故意使用一般命名函式方式來設計,如要改採用匿名函式做表達式之變數其寫法也差不多。
// 函式宣告 |
其 add2 的寫法不完整,雖然是透過推論出來的。如下列寫法說明解釋:=
的右側為匿名函式比較沒有問題,差別在於等號的左邊沒有給予宣告類型這是怎樣函式。所以右側將用到的傳遞與回傳之變數類型也要同樣宣告到左邊去,才能檢查傳遞進去的引數是哪種類型,也就是等號兩邊都要對應到。
// 補充 add2 的類型,其正確的完整寫法為 add3 |
TypeScript 中的
=>
和 ES6 中的=>
有所不同,在 TypeScript 邏輯上的=>
用來宣告 type is function 之傳遞與回傳區別定義。
如果只有一個傳遞引數,其實可以省略變數名:
let add4: (number) => number = function (x: number): number { |
搭配介面
也可以用介面來做成約束類型給予函式
interface User { |
搭配斷言
在複合類型搭配的可能下,需要處理某類型的動作可以額外採用斷言讓系統知道這是針對複合類型的指定類型之處理。
function getLength(arg: string | number): number { |
箭頭函式 Arrow Function
熟悉 JavaScript 函式表達式的話,其箭頭函式的用法就清楚了,唯獨=>
比較容易混淆需要特別注意一下。將上面的 add4 改成箭頭函式的寫法如下:
/* |
引數上使用箭頭函式
箭頭函式可以當作一個變數,既然是變數就能傳送到別的函式做承接使用。
function echo(num: number, fn: (number) => number) { //傳遞變數分別為數字與函式且都有宣告型態 |
類別 Class
類別 class 在 JavaScript ES6 開始提供使用。類別作為物件導向的物件設計藍圖應用。比較常見的在建構函式上對其使用建構子、屬性、方法其定義。一旦類別定義完成後,透過指定來獲得實例化物件。如果你還不熟悉 ES6 的類別,可一邊學習 TypeScript 的寫法與查看轉譯後的 JavaScript ES6 之 Class 寫法。
類別的規劃可以分為三個部分:
- attribute 屬性
為整個類別下可使用的變數,可先寫好也可以透過建構子來獲得外部引數為值。在類別內想要存取屬性都得需要透過 this 來導向至該變數位置。 - constructor 建構子
固定作為宣告時的特殊執行,當在外部使用 new 實例化物件時透過傳遞引數至建構子進行處理,藉此完成物件完成的初始前置動作。 - method 方法
類似類別下專有的函式,不需要寫 function 字眼,之後操作實例化物件時直接就能找到此函式來執行。
當設計完成時,透過 new 來實例化能呼叫這個建構式。
class info { //建立類別 class |
訪問修飾子 Access Modifier
指定類別內的屬性或方法是否可被別處使用。可前綴指定 public(預設)無限制、private(限自身 class)、protected(限自身 class 與 extends 繼承之子類別)。透過以下範例可以得到錯誤資訊說明:
class tryit { |
在 JavaScript ES6 版本還沒有存取修飾子這個觀念,因此 TypeScript 報錯後的編譯下都仍視同 public 存取,因此還需要搭配 Accessor 設計才完善。
修飾子也能用在建構子內的傳遞引數使用,等同於 class 的屬性之取代。直接接給屬性了
class tryit { |
另外一種是 readonly 的讀寫限制,可以跟訪問修飾子共存(注意寫位的順序)
class tryit { |
存取器 Accessor
假設屬性或方法已經設定 private 情況下,添加 Accessor 的功能達到寫入與讀取的唯一窗口。Accessor 採用 Method 方法的函式形式來設計,分為 get(讀)與 set(寫)關鍵字使用。將前段的例子做調整(簡化移除 protected 考量):
class tryit { |
get 視同唯讀不可有傳遞變數,而 set 視同唯寫不可有回傳變數。否則 TypeScript 會報錯。
如此一來 TypeScript 已符合存取條件不再報錯,同時這裡大多數的人不會額外命名,會把內部變數名稱與對外變數名稱以 _
一字之差來做提示自己。另外也可以添加通關密碼來做存取器的條件。
const |
繼承 extends
繼承如名詞解釋,可以新建立一個的子類別 class 來繼承父類別 class 的所有項目(屬性與方法)。在建立子類別當下描述寫入繼承自何處class newClass extends SourceClass {}
。
子類別除了來自父類別的繼承,也能添加自己特有的項目(但此觀念不可反向適用於父類別身上)。而子類別不能直接使用父類別的建構子,必需透過 super 關鍵字來呼叫父類別的建構子與方法。以下範例觀察父子類別之特性:
class father { |
抽象類別 abstract
對 class 指定為 abstract 模式,則能保護此 class 無法透過 new 實例化出來,只能被子 class 所繼承使用。
abstract class father { //抽象化無法被 new 使用 |
這裡雖然會報錯,但編譯後的 JavaScript 還是存在父 class。
複寫 Override
設計繼承時,如果 son 的方法跟來自 father 的方法撞名時,會發生複寫現象並以 son 自身為主。需特別注意:
class father { |
假設需要相同方法名稱下,你該考慮的方向很簡單,就是有兩個 class 可以新建構,你想選哪個做方法 print 使用:
class father { |
靜態 static
大多知道,透過實例化出來的物件的內容,可透過建構子在建構化過程中以 this 的物件導向觀念去修改值,也能實例化後再自行修改物件內容。
class father { |
然而如果一開始類別內的屬性或方法有設定 static 狀態時,這個對象就只限定給 class 使用,無法透過在外部從 this 導向呼喚出來。
class father { |
搭配介面
interface 如果套用在 class 的屬性也是一種約束作業,多個 class 除了繼承這種 1 對多個觀念,還有一種狀況為這些 class 彼此有相同非繼承的特性(屬性與方法),可以用介面來將這些共同特性設為約束產生完整的靈活性。
- class 要指定介面時必需要透過 implements 實現約束。
- class 可綁定多個介面,介面名稱使用
,
符號分開。例如class lokiwithface implements User, Company
interface User { //介面,約定持有 2+1 個屬性與方法 |
介面也可以反過來繼承自 class 認父來擴展介面內的屬性或方法。再提供給 object 或 class 做約束。
class User { |
介面能約束 class 的屬性與方法,也能約束
舉例 多個 class 有相同特性
能會發現一個介面對應一個 class 的使用意義不大,有點事是多此一舉的動作。介面真正的用途在於一個介面去約定多個 class 才是他真正價值。舉例且說明如下:
- 3 個 class 代表法術說明應用,1 個 class 代表使用法術之遊戲按鈕。
- clsBoth 這個 class 有前 2 個 class 相同用途。
- joycon 這個 class 會透過傳遞的實例化物件來執行法術的應用。
class clsAdd { //補血效果 |
在沒有介面的約束情況下,這些 class 等於是各自提供自己 type 來告知傳遞內容物為何。一旦加上介面整個物件導向的流程會明確清晰:
- 3 個法術 class 都是根據介面約束的定義 method 之應用。
- joycon 拿到傳遞引數之 type 不再是 cls* 之 class,而是這些共同約束 itf* 之介面當作 type。
interface itfAdd { |
如果問這兩段代碼的差異在哪,最大差別在於 type 是否是指向同一個宣告領域。
進階技巧
避免後續介紹太複雜,這裡先偷跑一些需要的 type 觀念做初步介紹,詳細用法會在各單元再出現時依據必要性提供說明。
別名機制
類型本身可以使用別名來登記,再透過套用別名方式來達到宣告類型。舉例如下:
type userName = string; |
字串字面值 (String Literal)
透過別名並指定限定的字串多個選擇限定。範例定義了一個字串字面值之類型為 EventNames 只接受三種字串中的一種。
type EventNames = 'click' | 'scroll' | 'mousemove'; |
泛型 Generics
泛型的應用就是將規劃類型當下不先做宣告,在變數名稱後綴先使用<關鍵字>
作替寫,等到執行函式時當下傳入的值再做類型說明。可適用於函式、介面、類別等各種場合出現。
用於函式
舉例以下設計 any 的傳遞引數與回傳如下。雖然整個函式都看似正常。如果 any 的思考則僅要求傳遞與回傳的類型是相同的,也就傳遞與回傳為 string and string[]
就跟目前的any and any[]
設計不適當。
function ary(value: any): any[] { //回傳 any 陣列 |
此時可透過泛型先做替代的類型,等到執行函式當下決定用什麼類型來決定傳遞與回傳類型。
function ary<T>(value: T): T[] { //函式名稱後面使用<T>來指定泛型,其中 T 為關鍵字。接著在函式內部包含傳遞回傳都能用 T 這個類型名 |
多個泛型
泛型能允許多個存在,使用符號,
來分開。
function swap<T, U>(val: [T, U]): [T, U] { //回傳元祖 |
泛型條件約束
另外有 2 個在函式上使用泛型的報錯現象值得討論,在使用泛型時在不清楚真實 type 之前,任何操作都可能因 type 不對盤而報錯。
function calc1<myType>(price: myType): myType { //myType 是泛用類型,作為暫定類型使用 |
原因為 myType 無法進行算術符號,因此透過繼承來獲得 number type 的形狀,使得該泛型能進行對盤的操作。
function calc2<myType extends number>(price: myType): myType { //可透過 extends 繼承 number,那麼這個 myType 初始情況下有 number 的定義可允許先接受數字計算 |
另外一個問題如下:
function long<myType>(str: myType): number { // 未知確定 string 情況下,沒有 length 的原生屬性 |
原因為 myType 沒有 length 這個屬性,因此 myType 透過繼承方式從介面來獲得 length 這個屬性形狀。注意這裡是用 extends 來繼承做形狀擴展,不是前面範例當作類型賦予。但如果傳進來資料不是 string 就不符合介面的約束報錯。
interface Plus{ |
最後一個技巧為泛型與泛型進行約束。下列函式設計用途說明如下:
- 將泛型 T 繼承至泛型 U,不討論兩者形狀是否雷同。我們最終目的為來源資料(套用泛型 U) 能判斷可否覆蓋至目標資料(套用泛型 T ) 上。
- 因為泛型 T 繼承了 U,所以 T 已受到約束必需持有 U 該有的屬性欄位。
- 當第一次函式執行時目標 T 類型為 a,b,c,而來源 U 為 b,c。因此程式可以正常執行。
- 而第二次函式執行時目標 T 類型為 a,b,c,而來源 U 為 d,c。因為 T 繼承了 U 所以應該需要 abcd 這些屬性。而傳入的 data 只有 abc 導致類型不符合產生報錯,達到偵測錯誤之目的。
function overWrite<T extends U, U>(target: T, source: U): T { |
用於搭配介面的函式
三者合併示範搭配介面的函式如何整合泛型的應用。透過前面找到搭配介面的函式進行步驟說明:
//由介面來約束 mySearch 的 type 形狀如何。 |
- 接著把 age 這個引數之類型 number 換成泛型名稱 T,有兩個地方要替換泛型 T。
- 函式綁定泛型如出一轍的替換;而介面的形狀寫法為 function 的形狀,因此介面綁泛型的方式與位置相同。
- 最後在函式執行的引數上指定泛型真正型態為 number,若省略此步驟其系統將自動推論出 number 給泛型。
interface User { |
- 介面綁泛型的方式也可寫在外面。缺點於 mySearch 這個類型指定就必需告知實際 type 不然會報錯。
interface User<T> { //變成是 User 這裡綁泛型 |
用於類別
用法差不多,從前面單元找範例調整一下來綁泛型:
class info { |
- 將 class 內的 string 替換成泛型,而 class 外面使用
<T>
做泛型綁定。 - class 內的所有 string 換成關鍵字 T
- new info 時可以告知泛型實際 type,或者讓系統推論
- obj 指定時可選擇告知為 class 類型,但必需要告知 class 的泛型 type
- 會發現 echo() 方法的回傳報錯,既使透過約束 extends string 也無效。
class info<T extends string> { |
改回固定的 string 回傳,畢竟很明顯只有 string 一種可能,也取消約束必要性。
class info<T> { |
泛型之預設 type
泛型可以預先綁定一組預設 type,如果外部沒有特別告知時會先嘗試自行推論,無法推論時會採用此預設 type
function ary<T = string>(value: T): T[] { |
宣告檔案
當專案內有使用一些第三方套件時,由於非 JavaScript 核心的關鍵名稱會導致 TypeScript 無法辨識產生錯誤。因此需要額外對這些關鍵名稱給予定義。舉例 jQuery 來解釋:
$("#title").html('hello world'); //找不到名稱 '$'。需要安裝 jQuery 的型別定義嗎?請嘗試 `npm i --save-dev @types/jquery`。 |
這裡不認識$
符號,但 TypeScript 回報懷疑是 jQuery 並建議你從第三方函式庫 npm 來安裝@type 擴充包來補充 jQuery 類型定義。跟著往下看:
獲取 npm 上的 @type
現在你可以嘗試使用 TypeScript 所提示的 npm 安裝 jQuery 類型定義擴展,透過 npm 來獲得指示上的指令,其中中的i
等價 install
。
npm i --save-dev @types/jquery |
如果你有手動宣告這些第三方套件的類型定義會告知衝突存在,例如「無法重新宣告區塊範圍變數 ‘$’」。移除手動的
.d.ts
檔案即可。
未來任何第三方套件都能嘗試從 npm 尋找現成的 type 定義擴展包。官方提供 TypeScript: Search for typed packages 網頁查詢。注意安裝來源的是@types/*
而不是套件本身。
宣告行為 declare
假設不透過 npm 獲取 npm 上的 @type,或在上面找不到第三方套件之類型定義包,只能手動來宣告檔案之類型定義。原理為在編譯檔案*.ts
之前會從檔案*.d.ts
讀取內容作為宣告的前置作業。因此你可以在這個環節設計一些做為全域類型作用的變數、函式、類別等物件。使得之後的 TypeScript 編譯當下能獲得認知。
宣告變數 declare var
最簡單的作法並同樣採用 jQuery 來示範。請移除從 npm 獲得的任何檔案目錄做練習。
- 我們需要將
$()
這個函式補充類型之形狀,就能正常解析並將$
當作一個(函數型)變數來處理 - 宣告的描述習慣上不會跟 TypeScript 之內容放在一起(當然堅持也可以)。另創立
*.d.ts
檔案可放置在任何位置。 - 每次編譯 TypeScript 會試著優先從專案內這些
*.d.ts
先理解宣告內容才會開始解析內容。
declare var $: (selector: string) => any; |
假如仍然無法解析,那麼可以檢查下
tsconfig.json
中的files
、include
和exclude
配置,確保其包含了jQuery.d.ts
檔案。
declare
作為宣告,接著變數 var,let,const 都可以給予$
這個變數,但注意使用 const 就無法再修改。建議用 const 避免宣告定義被改寫。
這裡為了練習方便都合併在 test.ts 上一起寫並注意順序,不再宣告寫至
*.d.ts
。
declare const $: (selector: string) => any; |
宣告函式 declare function
作為宣告一個全域方法。前例為$
當作一個函數型變數,這裡將$
當作一個函式(方法)來處理。如此一來,全域範圍下都認識這個 function 之類型。
declare function $(selector: string): any; |
也支援多載應用寫法
// in jQuery.d.ts |
宣告類別 declare class
這裡沒辦法用 jQuery 來說明,假設你已經持有一個來自第三方的 class 為plugin.js
且已透過<script src=>
加入專案,現在就是欠缺這個 class 的類型描述。
//這是現成的 js 且沒有 TypeScript 所需要的 Type |
const obj: user = new user('Loki', 18); // error: 找不到名稱 'user'。 |
因此 TypeScript 需要於全域部分進行宣告 class 之類型。
declare class user{ |
寫到這已開始明白當匯入自其他處的 js 檔案時,在 ts 檔案上嘗試使用該 js 資料物件因缺乏 type 而無法編譯,declare 就是根據不同物件類型有不同的用法。
宣告列舉 declare enum
同樣主要是在全域上宣告指定列舉之類型。
// in jQuery.d.ts |
宣告命名空間 declare namespace
用來表示一個組合性的自訂模組,例如 jQuery 有多種方法、屬性、建構式。可以透過命名空間一次組合起來宣告這些類型。
// in jQuery.d.ts |
命名空間可使用巢狀結構,再包一個命名空間之組合。
// in jQuery.d.ts |
其中的 fn 如果僅單獨抽取出來宣告命名,可以寫成以下宣告。
declare namespace $.fn { |
介面與別名
這裡不是使用 declare 來操作宣告,而是只是將介面與別名放置到*.d.ts
提升到外部作為全域類型使得整份專案都能讀到。
// in jQuery.d.ts |
然而提升到外部的宣告區則承擔了曝光風險,包含了可能取名上重複,可以技巧性地放置在已知的命名空間底下。
// in jQuery.d.ts |
宣告合併
當宣告不同物件類型且同名時是可以視同兩者合併不會影響。舉例 jQuery 來示範。
// in jQuery.d.ts |
開發套件之類型包
整理後餘時另編寫 [學習之路、] TypeScript 的進階篇 。有興趣請自詳閱 這裡
To Be Continued…