TypeScript 5 進階:新版裝飾器詳解

前言
在上一篇文章中,我們介紹了新版裝飾器與舊版裝飾器的差異。這一篇將更深入探討新版裝飾器的實際應用。要運行本文範例,請確保 TypeScript 版本為 5 以上,並且 experimentalDecorators 與 emitDecoratorMetadata 均未啟用或設為 false。
TypeScript 5 除了不再支援舊版的參數裝飾器(Parameter Decorator)外,還多增加了更多細節的裝飾器,目前裝飾器的種類有:
Class Decorator:類別裝飾器Method Decorator:方法裝飾器Field Decorator:屬性裝飾器Accessor Decorator:存取器裝飾器Getter Decorator:取值裝飾器Setter Decorator:設值裝飾器
類別裝飾器(Class Decorator)
TypeScript 5 類別裝飾器用法跟舊版來比較相差不多,只是多了一個 context 參數,並有提供 ClassDecoratorContext<T> 型別來定義 context。
下面的範例是使用裝飾器注入 createTime,並利用 interface 的自動型別合併功能擴充 createTime 型別。
function withCreateTime<T extends new (...args: any[]) => {}>(
baseClass: T,
context: ClassDecoratorContext<T>
) {
return class extends baseClass {
createTime: Date = new Date()
constructor(...args: any[]) {
super(...args)
}
}
}
@withCreateTime
class Person {
name: string = '阿毛'
}
interface Person {
createTime: Date
}
const person = new Person()
console.log(person.createTime) // Sat Sep 21 2024 19:24:58 GMT+0800 (台北標準時間)方法裝飾器(Method Decorator)
TypeScript 5 方法裝飾器用法跟舊版差很多,舊版的 target 會提供被裝飾類別的建構子,新版的 target 則是傳遞被裝飾的函式,且 propertyKey 與 descriptor 也移除了,改成使用型別 ClassMethodDecoratorContext<T, V> 定義的 context。
下面的範例我們使用的裝飾器工廠實作,並裝飾 addTodo 限制新增代辦事項的條件,就能在不改動類別的情況下,注入新的邏輯進去。
function limitTodo(limit: number) {
return function <
T extends { todos: Todo[] },
V extends (this: T, ...args: any) => any
>(target: Function, context: ClassMethodDecoratorContext<T, V>) {
return function (this: T, ...args: any[]) {
const unfinishedTodos = this.todos.filter(todo => !todo.finished)
if (unfinishedTodos.length < limit) {
return target.apply(this, args)
}
console.log('請先完成其他待辦事項')
}
}
}
interface Todo {
name: string
finished: boolean
}
class TodoList {
todos: Todo[] = []
@limitTodo(2)
addTodo(name: string) {
this.todos.push({ name, finished: false })
}
}
const todoList = new TodoList()
todoList.addTodo('吃飯')
todoList.addTodo('睡覺')
todoList.addTodo('打東東') // 請先完成其他待辦事項屬性裝飾器(Field Decorator)
TypeScript 5 屬性裝飾器就跟舊版的寫法差很多,舊版的 target 會提供被裝飾類別的建構子,新版的 target 則是傳遞 undefined,且新版可以回傳一個函式,這個函式會收到類別裝飾的屬性。
需注意此時的 this 會因為類別是在初始化的狀態下,所以當下有些值會是 undefined,不建議在裡面使用 this。
function initExampleTodos<T, V extends Todo[]>(
target: undefined,
context: ClassFieldDecoratorContext<T, V>
) {
return function (todos: V) {
todos.push({ name: '完成寫鐵人賽文章', finished: false })
return todos
}
}
interface Todo {
name: string
finished: boolean
}
class TodoList {
@initExampleTodos
todos: Todo[] = []
}
const todoList = new TodoList()
console.log(todoList.todos) // [{ name: "完成寫鐵人賽文章", finished: false }]存取器裝飾器(Accessor Decorator)
TypeScript 5 存取器裝飾器是新出的裝飾器,同時提供了三個型別讓我們可以定義,分別是 ClassAccessorDecoratorTarget、ClassAccessorDecoratorContext、ClassAccessorDecoratorResult,accessor 關鍵字是用來幫屬性自動設定存取器的,也就是會自動編譯成 getter 與 setter,而存取器裝飾器讓我們能用更細緻的方式去對屬性做裝飾。
下面的範例就是過去幾篇我們用屬性裝飾器來實作的範例,使用存取器裝飾器可以讓我們更快速優雅地做到這件事。
class Person {
@watchValue
accessor name: string = '阿毛'
}
function watchValue<T, V>(
target: ClassAccessorDecoratorTarget<T, V>,
context: ClassAccessorDecoratorContext<T, V>
): ClassAccessorDecoratorResult<T, V> {
return {
get(this: T) {
return target.get.call(this)
},
set(this: T, value: V) {
console.log(`設置 ${context.name.toString()} 屬性值從 ${target.get.call(this)} 改為 ${value}`)
target.set.call(this, value)
}
}
}
const person = new Person()
person.name = 'Mao' // 設置 name 屬性值從 阿毛 改為 MaoGetter Decorator 與 Setter Decorator
TypeScript 5 還有針對 Getter 與 Setter 做出新的裝飾器,用法也非常簡單,所以將這兩個裝飾器一起說明,target 都是各自的 getter 或 setter 函式,需要注意的是,因為都是對物件操作,所以要透過 call、apply 或 bind 的方式來綁定 this 來執行。
class Person {
private _name: string = '阿毛'
@greet
get name() {
return this._name
}
@logSetter
set name(value) {
this._name = value
}
}
function greet<T, V>(
target: () => V,
context: ClassGetterDecoratorContext<T, V>
) {
return function (this: T) {
return `我的名字是 ${target.call(this)}`
}
}
function logSetter<T, V>(
target: (value: V) => void,
context: ClassSetterDecoratorContext<T, V>
) {
return function (this: T, value: V) {
console.log(`名字改成 ${value}`)
target.call(this, value)
}
}
const person = new Person()
console.log(person.name) // 我的名字是 阿毛
person.name = 'Mao' // 名字改成 Mao
console.log(person.name) // 我的名字是 Mao總結
在這篇文章中,我們詳細介紹了新版 TypeScript 5 裝飾器的實際應用,包括類別、方法、屬性、存取器及其細化的 getter 和 setter 裝飾器。
參考資料
相關文章
TypeScript 後記
終於完成連續三十天的鐵人賽文章撰寫!距離我第一次參加鐵人賽已經三年之久了。這次參加主要是因為朋友邀請一起參賽,同時我自己也想藉此機會深入了解 TypeScript 5 的新功能,強迫自己透過寫作來吸收知識。
TypeScript 實戰:製作 npm Package
在開發專案時,經常會遇到跨專案共用邏輯或工具的情況,例如常用的 logger 或驗證工具。如果每次都從一個專案複製貼上到另一個專案,難免會出現版本不同步或維護困難的問題。為了解決這類問題,我們可以將這些共用邏輯獨立成為 npm 套件,以便在多個專案中重複使用。
TypeScript 實戰:前端表單與串接 API
在現代前端開發中,與後端進行資料交換是常見的需求。透過 HTTP 請求,我們可以將資料送出並取得結果。這篇文章將介紹如何使用 TypeScript 實作前端表單與串接 API,並展示如何將這些功能結合到 React 應用中。同時,文章還會介紹如何解決跨域問題(CORS),以便讓前後端能順利溝通。