TypeScript 進階:舊版裝飾器

前言
在上一篇文章中,我們初步介紹了裝飾器的基本概念與應用方式。本篇將深入探討 TypeScript 5 之前的舊版裝飾器,並介紹其各種類型。在舊版裝飾器中,有些情況下需要在 tsconfig.json 中額外設定 emitDecoratorMetadata、experimentalDecorators 來啟用裝飾器元資料。請先將此設定加入,以便順利使用裝飾器功能。
類別裝飾器(Class Decorator)
類別裝飾器可以用來修改或擴展類別的定義。它可以用來新增資料、修改原有的類定義或對類進行封裝處理。類別裝飾器會傳原本的類別為參數,並加在要使用此裝飾器的類別上方。
@sealed
@withAge
class Person {
name: string = '阿毛'
}
function sealed<T extends new (...args: any[]) => {}>(baseClass: T) {
console.log('阻止擴展類別裝飾器')
Object.seal(baseClass)
Object.seal(baseClass.prototype)
}
function withAge<T extends new (...args: any[]) => {}>(baseClass: T) {
console.log('套用年齡類別裝飾器')
return class extends baseClass {
age: number = 18
constructor(...args: any[]) {
super(...args)
console.log(`套用年齡到 ${baseClass.name} 類別`)
}
}
}如果泛型要約束可實例化類別,可以使用 new (...args: any[]) => {} 來進行約束。
多個裝飾器的執行順序
可以看到裝飾器可以套用多個,以上面那個案例,會先執行 withAge 在執行 sealed,裝飾器會按照從下到上的順序執行,這或許有些反直覺,但如果將裝飾器看作嵌套的函數調用,這個順序就變得合乎邏輯。例如:
// 以下只是幫助想像的範例
sealed(
withAge(
Person
)
)裝飾器工廠(Decorator Factory)
看到上方範例中使用的 withAge,是不是覺得寫死年齡是不是不夠彈性,這時候就可以使用裝飾器工廠的撰寫方式,裝飾器工廠允許我們動態生成裝飾器,通過傳遞參數來控制裝飾器的行為。而裝飾器工廠本質上,其實就是一個返回裝飾器函數的函數。
@withAge(18)
class Person {
name: string = '阿毛'
}
function withAge(_age: number) {
return function<T extends new (...args: any[]) => {}>(baseClass: T) {
console.log('套用年齡類別裝飾器')
return class extends baseClass {
age: number = _age
constructor(...args: any[]) {
super(...args)
console.log(`套用年齡到 ${baseClass.name} 類別`)
}
}
}
}可以看到我們透過函數的方式更彈性的傳遞參數進去。
方法裝飾器(Method Decorator)
方法裝飾器用來修改方法的行為,例如在方法執行前後進行邏輯操作、修改方法的返回值或記錄方法的執行過程。方法裝飾器會傳三個參數,分別為 target(被裝飾類別的建構子) 、 key(被裝飾類別的方法名稱)與 descriptor(被裝飾類別的方法屬性),並加在要使用此裝飾器的類別方法上方。
class Wallet {
private balance: number = 0
constructor(public limit: number) {}
@limitDeposit
addMoney(amount: number) {
this.balance += amount
console.log(`已存入 ${amount} 元,目前餘額:${this.balance} 元`)
}
}
// 方法裝飾器來限制單次存款上限
function limitDeposit<T extends { limit: number }>(target: T, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (this: T, amount: number) {
if (amount > this.limit) {
console.log(`存款失敗:單次存款不能超過 ${this.limit} 元`)
}
else {
originalMethod.apply(this, [amount])
}
}
}
const myWallet = new Wallet(500) // 設定單次存款上限為 500 元
myWallet.addMoney(100) // 已存入 100 元,目前餘額:100 元
myWallet.addMoney(600) // 存款失敗:單次存款不能超過 500 元屬性裝飾器(Property Decorator)
屬性裝飾器可以用來修飾類別的屬性,可以攔截屬性的讀取(getter)和寫入(setter)行為。屬性裝飾器會傳兩個參數,分別為 target(被裝飾類別的建構子) 與 key(被裝飾類別的屬性名稱),並加在要使用此裝飾器的類別屬性上方。
class Person {
@watchValue
name: string = '阿毛'
}
function watchValue<T, K extends keyof T & string>(target: T, key: K) {
console.log('屬性裝飾器')
let value = target[key]
const getter = () => value
const setter = (newValue: T[K]) => {
console.log(`設置 ${key} 屬性值從 ${value} 改為 ${newValue}`)
value = newValue
}
Object.defineProperty(target, key, {
get: getter,
set: setter,
configurable: true,
enumerable: true,
})
}
const person = new Person()
person.name = 'Mao'參數裝飾器(Parameter Decorator)
參數裝飾器用於修飾方法的參數,這種裝飾器通常用來標註參數或進行參數的驗證。參數裝飾器會傳三個參數,分別為 target(被裝飾類別的建構子) 、 key(被裝飾類別的屬性名稱)與 index(參數的順序),並加在要使用此裝飾器的參數前方。
因為使用情境不多,且 TypeScript 5 把這個裝飾器移除了,所以這邊不用特別放在心上。
class Wallet {
private balance: number = 0
addMoney(@logParameter amount: number) {
this.balance += amount
}
}
function logParameter<T>(target: T, key: string, index: number) {
console.log(`${key} 方法的第 ${index + 1} 個參數`)
}總結
本文介紹了 TypeScript 5 之前的舊版裝飾器,包括類別裝飾器、方法裝飾器、屬性裝飾器、參數裝飾器、裝飾器工廠與裝飾器執行的順序。這些裝飾器提供了一種在不更改原始邏輯的前提下,對類及其成員進行修改和擴展的靈活方式。隨著 TypeScript 5 的更新,裝飾器功能得到了進一步加強,下一篇將介紹 TypeScript 5 的新版裝飾器的使用方式。
參考資料
相關文章
TypeScript 後記
終於完成連續三十天的鐵人賽文章撰寫!距離我第一次參加鐵人賽已經三年之久了。這次參加主要是因為朋友邀請一起參賽,同時我自己也想藉此機會深入了解 TypeScript 5 的新功能,強迫自己透過寫作來吸收知識。
TypeScript 實戰:製作 npm Package
在開發專案時,經常會遇到跨專案共用邏輯或工具的情況,例如常用的 logger 或驗證工具。如果每次都從一個專案複製貼上到另一個專案,難免會出現版本不同步或維護困難的問題。為了解決這類問題,我們可以將這些共用邏輯獨立成為 npm 套件,以便在多個專案中重複使用。
TypeScript 實戰:前端表單與串接 API
在現代前端開發中,與後端進行資料交換是常見的需求。透過 HTTP 請求,我們可以將資料送出並取得結果。這篇文章將介紹如何使用 TypeScript 實作前端表單與串接 API,並展示如何將這些功能結合到 React 應用中。同時,文章還會介紹如何解決跨域問題(CORS),以便讓前後端能順利溝通。