decorator הוא פונקציה שמצמידים למחלקה או לאיבר של מחלקה עם @name. הוא מקבל את המתודה המקורית (או את המחלקה, או את השדה) ואובייקט הקשר שמתאר אותה, והוא יכול להחזיר תחליף. TypeScript תומכת ב-decorators הסטנדרטיים בלי שום דגל קומפיילר.
@logged רץ פעם אחת, כשהמחלקה מוגדרת, ומחליף את add בעטיפה שהוא מחזיר. כל קריאה עוברת דרך העטיפה. הפרמטרים הגנריים שומרים על הטיפוסים של this, של הארגומנטים ושל ערך ההחזרה של המתודה, כך ש-add עדיין מקבלת שני מספרים ומחזירה מספר.
איך decorators מתקמפלים
decorators סטנדרטיים מגיעים מהצעה של TC39 ל-JavaScript, ו-TypeScript מממשת אותם מאז TypeScript 5.0. ההצעה עוד לא חלק מתקן JavaScript, ו-Node 24 לא מפענח את התחביר @, ולכן כשה-target הוא ES2022 (כמו בעמודים האלה) הקומפיילר כותב מחדש כל מחלקה מעוטרת ל-JavaScript רגיל שקורא לפונקציות עזר (__esDecorate ו-__runInitializers, שנכתבות בראש הקובץ). הפלט רץ בכל מקום שבו ES2022 רץ.
השלכה אחת: קובץ עם decorators לא יכול לרוץ תחת ה-type stripping המובנה של Node (node file.ts), שרק מסיר טיפוסים ומשאיר את ה-@ במקומו. Node עוצר עם SyntaxError: Invalid or unexpected token. קמפלו אותו קודם עם tsc או עם bundler.
סוגי decorators והחתימות שלהם
לכל decorator סטנדרטי יש את הצורה (value, context) => replacement | void. מה זה value, ומה מותר להחזיר, תלוי במה שמעוטר:
| מעטר | value | טיפוס ההקשר | ערך החזרה |
|---|---|---|---|
| מחלקה | המחלקה | ClassDecoratorContext | מחלקה חלופית, או כלום |
| מתודה | המתודה | ClassMethodDecoratorContext | מתודה חלופית |
| getter / setter | ה-getter או ה-setter | ClassGetterDecoratorContext / ClassSetterDecoratorContext | getter או setter חלופי |
| שדה | undefined | ClassFieldDecoratorContext | פונקציה שממפה את הערך ההתחלתי |
שדה accessor | { get, set } | ClassAccessorDecoratorContext | { get?, set?, init? } |
לכל אובייקט הקשר יש kind, name ו-addInitializer. להקשר של איבר מחלקה יש גם static, private ואובייקט access לקריאת האיבר ממופע. decorators עובדים גם על איברים סטטיים ועל איברי #private.
Decorator Factories
כדי להעביר אפשרויות, כתבו פונקציה שמחזירה decorator וקראו לה במקום של ה-@. זה decorator factory:
@retry(3) קורא קודם ל-retry, והפונקציה שהיא מחזירה היא ה-decorator בפועל. כמה decorators נערמים זה על זה: ב-@a @b method(), b מופעל ראשון ו-a עוטף את התוצאה.
Class Decorators ו-addInitializer
class decorator מקבל את המחלקה עצמה. הוא יכול להחזיר תת-מחלקה שתחליף אותה, או לא להחזיר כלום ורק לרשום אותה איפשהו. context.addInitializer רושם קוד שירוץ ברגע מסוים: ב-class decorator, מיד אחרי שהמחלקה מוגדרת במלואה; ב-method decorator, כשכל מופע נבנה.
ה-class decorator נכתב מעל המחלקה (או אחרי export). בלי @bound, קריאה ל-loose() הייתה זורקת, כי this היה undefined.
Field Decorators ו-Accessor Decorators
field decorator לא יכול לראות או ליירט השמות מאוחרות יותר: ה-value שלו הוא undefined, וכל מה שהוא יכול להחזיר הוא פונקציה שמשנה את הערך ההתחלתי של השדה. כדי ליירט קריאות וכתיבות, הצהירו על השדה עם מילת המפתח accessor, שהופכת אותו לזוג של getter ו-setter שמגובה באחסון פרטי, ועטרו אותו:
accessor הוא חלק מאותה הצעה. הוא מייצר getter ו-setter אמיתיים מעל שדה #private, ובגלל זה p.price = -5 עובר דרך ה-set של ה-decorator.
סטנדרטיים מול experimentalDecorators הישנים
לפני TypeScript 5.0, ה-decorators היחידים שהיו ל-TypeScript היו גרסה מוקדמת של ההצעה, שהופעלה עם experimentalDecorators. הדגל הזה עדיין קיים, והוא מעביר את הקומפיילר למודל הישן, עם חתימות וסמנטיקה שונות:
| סטנדרטי (בלי דגל) | ישן (experimentalDecorators) | |
|---|---|---|
| חתימה | (value, context) | (target, propertyKey, descriptor) |
| איך הוא משנה מתודה | מחזיר פונקציה חדשה | משנה את descriptor.value |
| Parameter decorators | לא נתמכים (TS1206) | נתמכים |
emitDecoratorMetadata | לא נתמך | נתמך (מידע על טיפוסים בזמן ריצה דרך reflect-metadata) |
decorators של accessor ({ get, set, init }), addInitializer | כן | לא |
| מבוסס על | ההצעה של TC39 | טיוטה ישנה שלה |
decorator שנכתב למודל אחד לא עובר בדיקת טיפוסים במודל השני. הנה decorator בסגנון הישן בפרויקט בלי הדגל:
index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.
התיקון הוא לכתוב אותו מחדש בצורה הסטנדרטית (value, context), כמו בדוגמה הראשונה בעמוד הזה, או להפעיל את המודל הישן לכל הפרויקט:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Angular, NestJS ו-TypeORM עדיין מגדירים experimentalDecorators בקונפיגורציות שהכלים שלהם מייצרים ושהתיעוד שלהם דורש. NestJS ו-TypeORM צריכים גם emitDecoratorMetadata, כי הם קוראים טיפוסים בזמן ריצה: NestJS כדי להזריק פרמטרים של בנאי, TypeORM כדי למפות מאפיינים לעמודות:
// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
constructor(@Inject(DB) private db: Database) {}
}
אם אתם משתמשים באחד מה-frameworks האלה, כתבו decorators בדרך הישנה ועקבו אחרי התיעוד שלו. לקוד חדש בלי framework כזה, השתמשו ב-decorators הסטנדרטיים.
מתי להשתמש ב-decorators
decorators מתאימים להתנהגות רוחבית שאחרת הייתה חוזרת על עצמה בהרבה מתודות: לוגים, מדידת זמן, caching, ניסיונות חוזרים, בדיקות הרשאה, ולידציה ורישום מחלקות ב-container או ב-router. הם גם מסתירים את זרימת הבקרה, כי קורא צריך לבדוק מה @retry עושה לפני שהוא יודע מה המתודה עושה. לשימוש אחד או שניים, higher-order function רגילה (const fetchData = retry(3, rawFetch)) פשוטה יותר ועובדת גם מחוץ למחלקות.
שאלות נפוצות
מה הם decorators ב-TypeScript?
decorator הוא פונקציה שמופעלת על מחלקה או על איבר של מחלקה בתחביר @name. הוא מקבל את מה שמעוטר ואובייקט הקשר (context), ויכול להחזיר תחליף: מתודה עטופה, מחלקה חדשה, או פונקציה שמשנה את הערך ההתחלתי של שדה. שימושים נפוצים הם לוגים, ולידציה, caching ורישום מחלקות.
האם צריך experimentalDecorators כדי להשתמש ב-decorators ב-TypeScript?
לא. מאז TypeScript 5.0, ה-decorators הסטנדרטיים (TC39) עובדים בלי דגל. experimentalDecorators מעביר את הקומפיילר למודל ה-decorators הישן (legacy), שעליו בנויים frameworks כמו Angular ו-NestJS. לשניהם יש חתימות פונקציה שונות, ואי אפשר להחליף אחד בשני.
מה ההבדל בין decorators סטנדרטיים ל-experimentalDecorators?
decorators סטנדרטיים מקבלים (value, context) ומחזירים תחליף. decorators ישנים מקבלים (target, propertyKey, descriptor) ומשנים את ה-property descriptor. רק המודל הישן תומך ב-parameter decorators וב-emitDecoratorMetadata; רק במודל הסטנדרטי יש decorators של accessor שמחזירים { get, set, init } ואת context.addInitializer.
האם TypeScript תומכת ב-parameter decorators?
רק כש-experimentalDecorators מופעל. במצב הסטנדרטי, decorator על פרמטר הוא השגיאה TS1206: Decorators are not valid here, כי ההצעה של TC39 לא כוללת parameter decorators. לכן frameworks של dependency injection שמעטרים פרמטרים של בנאי צריכים את הדגל הישן.
באיזה סדר מופעלים כמה decorators?
הביטויים של ה-decorators מחושבים מלמעלה למטה, אבל מופעלים מלמטה למעלה: ב-@a @b method(), b עוטף את המתודה ראשון ו-a עוטף את התוצאה. לכן a רץ בשכבה החיצונית כשקוראים למתודה.