دنیای برنامه نویسی همیشه در حال پیشرفت می باشد و هر روز موارد جدیدی به آن اضافه می شود که یکی از این موارد که می خواهیم به آن بپردازیم دکوراتور ها در تایپ اسکریپت می باشد. The State of Developer Ecosystem 2022 تایپ اسکریپت را به عنوان زبان برنامه نویسی که سریعترین رشد را دارد معرفی کرد. سخت نیست که علتش را. این ابرمجموعه محبوب جاوا اسکریپت چک کردن نوع، enums و سایر پیشرفتها را فراهم میکند. اما گاها TypeScript ویژگیهای مورد انتظار را معرفی میکند که هنوز بخشی از استاندارد ECMAScript که جاوا اسکریپت بر آن تکیه دارد، نیستند. و این باعث می شود که اکثر جامعه برنامه نویس به سمت این زبان بروند.
برای مثال، معرفی مجدد دکوراتورها در TypeScript 5.0 است که به زودی منتشر خواهد شد. decorators یک تکنیک متا برنامه نویسی است که در سایر زبان های برنامه نویسی نیز وجود دارد. اگر یک توسعهدهنده برنامه یا پکیج نویسی می کنید و علاقهمند به استفاده از دکوراتورهای رسمی TypeScript جدید هستید، باید از سینتکس جدید آن استفاده کنید و تفاوتهای بین مجموعه ویژگیهای قدیمی و جدید را درک کنید. تفاوت های API بسیار زیاد است و بعید است که دکوراتورهای قدیمی با سینتکس جدید خارج از باکس کار کنند.
در این مقاله، تاریخچه استفاده از دکوراتورها در TypeScript را مرور میکنیم، مزایای مرتبط با دکوراتورها را در TypeScript 5.0 مورد بحث قرار میدهیم، سپس می آیم استفاده از دکوراتورهای مدرن را یاد بگیریم و چگونگی اصلاح دکوراتورهای موجود را بررسی میکنیم.
قبل از هر چیزی توجه داشته باشید، همه APIها در TypeScript 5.0 تغییرات زیادی کرده اند. برای این مقاله، ما بر روی دکوراتورهای متد کلاس تمرکز خواهیم کرد.
تاریخچه دکوراتورهای TypeScript
Decorators قابلیتی است که توسعه دهندگان را قادر می سازد تا با افزودن سریع عملکرد به کلاس ها، ویژگی های کلاس و متدهای کلاس، و تکرار را کاهش می دهد. زمانی که TypeScript برای اولین بار دکوراتورها را معرفی کرد، از مشخصات ECMAScript پیروی نمی کرد. این برای توسعه دهندگان زیادی جالب نبود، زیرا کدهای ارسال شده از هر کامپایلر جاوا اسکریپت باید با استانداردهای وب مطابقت داشته باشد.
استفاده از دکوراتورها مستلزم تنظیم یک دستور کامپایلر آزمایشی experimentalDecorators--
است. چندین کتابخانه محبوب TypeScript، مانند type-graphql و inversify، بر این پیاده سازی متکی هستند.
در اینجا مثالی از یک دکوراتور با متد کلاسی ساده آورده شده است که ارگونومی پیشرفته سینتکس جدید را نشان می دهد:
function debugMethod(_target: unknown, memberName: string, propertyDescriptor: PropertyDescriptor) {
return {
get() {
const wrapperFunction = (...arguments_: unknown[]) => {
const now = new Date(Date.now());
console.log('start time', now.toISOString());
propertyDescriptor.value.apply(this, arguments_);
const end = new Date(Date.now());
console.log('end time', end.toISOString());
};
Object.defineProperty(this, memberName, {
value: wrapperFunction,
configurable: true,
writable: true,
});
return wrapperFunction;
},
};
}
class ComplexClass {
@debugMethod
public complexMethod(a: number): void {
console.log("DOING COMPLEX STUFF!");
}
}
در کد بالا، می بینیم که debugMethod
دکوراتور ویژگی متد کلاس را با استفاده از Object.defineProperty
می آید overrides می کند، اما به طور کلی، دنبال کردن کد آسان نیست. همچنین، آرگومان ها از نظر تایپ ایمن نیستند، که ایمنی ما را در داخل wrapperFunction
محدود می کند. علاوه بر این، کامپایلر در صورت استفاده از این دکوراتور در مورد استفاده نامعتبر، مانند ویژگی کلاس، خراب نخواهد شد.
ما میتوانیم از ژنریکهای TypeScript برای دستیابی به ایمنی تایپ استفاده کنیم، اما TypeScript تایپ های عمومی را استنباط نمیکند و این باعث میشود مصرف آنها دردسرساز باشد. بنابراین، نوشتن دکوراتورهای پیچیده به دلیل مقادیر ناشناخته ای که کاربران می توانند در آنها وارد کنند، دشوار است.
نسخه مدرن دکوراتورها که به طور رسمی در TypeScript 5.0 عرضه خواهد شد، دیگر نیازی به دستور کامپایلر ندارد و از پیشنهاد رسمی ECMAScript Stage-3 پیروی می کند. در کنار یک پیاده سازی پایدار که از استانداردهای ECMAScript پیروی می کند، دکوراتورها اکنون به طور یکپارچه با سیستم تایپ TypeScript کار می کنند و عملکردهای پیشرفته تری را نسبت به نسخه اصلی امکان پذیر می کنند.
با پیاده سازی جدید دکوراتورها در TypeScript 5.0، این جنبه ها تا حد زیادی بهبود یافته است. بریم یک نگاهی به آن بیندازیم.
دکوراتورها در TypeScript 5.0
TypeScript 5.0 ارگونومی بهتر، ایمنی تایپ بهبود یافته و موارد دیگر را ارائه می دهد. در اینجا یک مثال مشابه از دکوراتور TypeScript 5.0 است که یک متد کلاس را overrides می کند:
function debugMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
const now = new Date(Date.now());
console.log('start time', now.toISOString());
const result = originalMethod.call(this, ...args);
const end = new Date(Date.now());
console.log('end time', end.toISOString());
return result;
}
return replacementMethod;
}
class ComplexClass {
@debugMethod
complexMethod(a: number): void {
console.log("DOING STUFF!");
}
}
دقت کنید که برای امتحان TypeScript به صورت آنلاین، فقط نسخه را به "nightly" یا "5.0" تغییر دهید.
با پیاده سازی جدید، به سادگی برگرداندن تابع می تواند جایگزین آن شود. نیازی به Object.defineProperty
نیست. این کار دکوراتورها را برای پیاده سازی و درک آن آسان تر می کند. در کنار این بهبود، بیایید آن را کاملاً ایمن کنیم:
function debugMethod<TThis, TArgs extends [string, number], TReturn extends number>(
originalMethod: Function,
context: ClassMethodDecoratorContext<TThis, (this: TThis, ...args: TArgs) => TReturn>
) {
function replacementMethod(this: TThis, a: TArgs[0], b: TArgs[1]): TReturn {
const now = new Date(Date.now());
console.log('start time', now.toISOString());
const result = originalMethod.call(this, a, b);
const end = new Date(Date.now());
console.log('end time', end.toISOString());
return result;
}
return replacementMethod;
}
عملکرد دکوراتورها در TypeScript 5.0 بسیار بهبود یافته است و اکنون موارد زیر را پشتیبانی می کند:
- استفاده از ژنریک برای تایپ آرگومان های یک متد و برگرداندن یک مقدار؛ متد باید یک رشته و یک عدد،
TArgs
را بپذیرد و یک عددTReturn
را برگرداند. - تایپ
originalMethod
به عنوان یک تابع. - با استفاده از نوع کمکی داخلی
ClassMethodDecoratorContext
. این برای همه تایپ های دکوراتور وجود دارد.
ما میتوانیم با بررسی خطاها در صورت استفاده نادرست، آزمایش کنیم که ببینیم آیا دکوراتور ما واقعاً از نظر تایپ ایمن است یا خیر.
دمو کارخانه دکوراتور
ما میتوانیم از ایمنی تایپ موجود در دکوراتورهای TypeScript 5.0 برای ایجاد عملکردهایی استفاده کنیم که دکوراتور را برمیگردانند، که در غیر این صورت به عنوان کارخانه دکوراتورها شناخته میشود. کارخانه های دکوراتور به ما این امکان را می دهند که با عبور دادن برخی از پارامترها در کارخانه، رفتار دکوراتورهای خود را سفارشی کنیم.
برای نسخه ی نمایشی خود، یک کارخانه دکوراتور ایجاد می کنیم که آرگومان متد کلاس را بر اساس آرگومان های خودش تغییر می دهد. این کار با عملگر سه تایی تایپ TypeScript امکان پذیر است. مثال ما از چارچوب های REST API مانند NestJS می باشد.
ما ماژول خود را rest-framework
می نامیم. بیایید با ایجاد یک پروژه TypeScript خالی با استفاده از ts-node
شروع کنیم:
$ mkdir rest-framework
$ cd rest-framework
$ npm init -y
$ npm install -D typescript@5.0.4 @types/node ts-node
$ touch index.ts
$ echo "console.log('Hello, world!');" > index.ts
در مرحله بعد، اسکریپت را برای ساخت و اجرای پروژه در package.json
تعریف می کنیم:
{
// ...
"scripts": {
"build": "tsc",
"start": "ts-node index.ts"
}
}
سپس npm را اجرا کنیم تا آن را ببینیم:
$ npm start
Hello, world!
حالا تایپ های خود را تعریف کنیم:
interface RouteOptionsAuthEnabled {
auth: true;
}
interface RouteOptionsAuthDisabled {
auth: false;
}
type RouteArguments = [string] | [];
type RouteDecorator<TThis, TArgs extends RouteArguments> = (
originalMethod: Function,
context: ClassMethodDecoratorContext<
TThis,
(this: TThis, ...args: TArgs) => string
>
) => void;
بعد دکوراتور کارخانه را تعریف کنیم:
function Route<
TThis,
// The user can enable or disable auth
TOptions extends RouteOptionsAuthEnabled | RouteOptionsAuthDisabled
>(
options: TOptions
): RouteDecorator<
TThis,
// Do not accept a function that uses a string for an argument if auth is disabled
TOptions extends RouteOptionsAuthEnabled ? [string] : []
> {
return <TThis>(
target: (
this: TThis,
...args: TOptions extends RouteOptionsAuthEnabled ? [string] : []
) => string,
context: ClassMethodDecoratorContext<
TThis,
(
this: TThis,
...args: TOptions extends RouteOptionsAuthEnabled ? [string] : []
) => string
>
) => {};
}
اکنون یک کارخانه روت داریم که بسته به گزینه های کاربر، انواع پارامتر متد کلاس را تغییر می دهد.
بیایید یک کلاس Route ایجاد کنیم تا به عنوان مورد آزمایشی ما عمل کند:
class Controller {
@Route({ auth: true })
get(authHeaderValue: string): string {
console.log("get http method handled!");
return "response";
}
@Route({ auth: false })
post(): string {
console.log("post http method handled!");
return "response";
}
}
اگر بخواهیم از authHeaderValue
در روت پست استفاده کنیم، می بینیم که TypeScript در کامپایل شکست میخورد.
استفاده از کارخانه دکوراتور یک مثال ساده است، اما قدرت کاری که دکوراتورهای ایمن می توانند انجام دهند را نشان می دهد.
بازسازی دکوراتورهای موجود
اگر از دکوراتور TypeScript موجود استفاده میکنید، میخواهید از API استفاده کنید و از مزایای فراوان آن استفاده کنید. دکوراتورهای اصلی را می توان به راحتی به دکوراتورهای جدید بازسازی کرد، اما تفاوت آنقدر قابل توجه است که نیاز به تلاش دارد.
برای بهترین نتیجه، مراحل زیر را برای بازسازی دکوراتورهای موجود دنبال کنید:
- یونیت تست ها را برای دکوراتورهای خود بنویسید.
- دستور های کامپایلر ExperimentalDecorators TypeScript را حذف کنید.
- این خلاصه گسترده از سینتکس عملکرد پیشنهاد جدید را بخوانید
- محدودیت های دکوراتورهای مدرن را درک کنید (در ادامه این مقاله به این موضوع خواهیم پرداخت).
- دکوراتورها را بدون هیچ تایپی بازنویسی کنید و از هر کدام به جای همه تایپ ها استفاده کنید.
- مطمئن شوید که تست های یونیت را قبول می شوند.
- تایپ ها را اضافه کنید.
آشنایی با محدودیتهای دکوراتورهای مدرن
اجرای مدرن دکوراتور خبر خوبی برای توسعه دهندگان TypeScript است، اما ویژگی های قابل توجهی هم ندارد. اول، هیچ پشتیبانی از پارامترهای روش داکوراتور وجود ندارد. این در مشخصات پیشنهاد است، بنابراین امیدواریم در مشخصات نهایی گنجانده شود. حذف آن قابل توجه است زیرا کتابخانه های محبوب، مانند type-graphql
، از آن در راه های مهمی مانند حل کننده های نوشتن استفاده می کنند:
@Query(returns => Recipe)
async recipe(@Arg("recipeId") recipeId: string) {
return this.recipeRepository.findOneById(recipeId);
}
دوم، TypeScript 5.0 نمی تواند متادیتای دکوراتور را منتشر کند. پس از آن، با Reflect API یکپارچه نمی شود و با پکیج npm reflect-metadata کار نمی کند.
سوم، دستور emitDecoratorMetadata--
، که قبلا برای دسترسی و اصلاح ابرداده برای دکوراتورهای خاص استفاده می شد، دیگر پشتیبانی نمی شود. متأسفانه، هیچ راهی واقعی برای دستیابی به همان عملکرد با دریافت ابرداده در زمان اجرا وجود ندارد. مواردی وجود دارد که می توان آنها را بازسازی کرد. به عنوان مثال، اجازه دهید یک دکوراتور تعریف کنیم که انواع پارامترهای یک تابع را در زمان اجرا تأیید می کند:
function validateParameterType(target: any, propertyKey: string | symbol): void {
const methodParameterTypes: (string | unknown)[] =
Reflect.getMetadata("design:paramtypes", target, propertyKey) ?? [];
const firstParameterType = methodParameterTypes[0];
if (typeof firstParameterType !== "string") {
throw new TypeError("First parameter must be a string");
}
}
ما می توانیم عملکرد مشابهی را با ایمنی تایپ بهبود یافته ارائه شده توسط TypeScript 5.0 به دست آوریم. ما به سادگی آرگومان های روشی را که دکوراتور می کنیم اضافه می کنیم، مانند:
function debugMethod<TThis, TArgs extends [string], TReturn>(
) {
...
در تئوری، ما میتوانیم از این رویکرد برای دکوراتورهایی استفاده کنیم که به دریافت انواع از Reflect برای design:type
، design:paramtypes
و design:returntype
بستگی دارند. این یک روش متفاوت برای نوشتن دکوراتورها است. این یک refactor ساده نیست، زیرا نیاز به استفاده از نوع استنتاج TypeScript برای احیا کردن سینتمس کسب و اعتبارسنجی تایپ دارد.
دکوراتور ها در TypeScript
اجرای جدید دکوراتور در TypeScript 5.0 از پیشنهاد رسمی ECMAScript Stage-3 پیروی می کند و اکنون از نظر تایپ ایمن است و پیاده سازی و درک آن را آسان تر می کند. با این حال، برخی از ویژگی های قابل توجه مانند پشتیبانی از پارامترهای روش دکوراتور و توانایی انتشار ابرداده های دکوراتور وجود ندارد.
دکوراتورهای پایه را می توان به راحتی به نسخه TypeScript 5.0 تغییر داد، اما موارد استفاده پیشرفته به تلاش بیشتری نیاز دارد. توسعه دهندگان می توانند دکوراتورهای موجود را اصلاح کنند تا از API جدید استفاده کنند و از مزایای مرتبط بهره ببرند. آنها میتوانند کمتر به کتابخانههای خارجی وابسته باشند و کمتر احتمال دارد که کد را در آینده اصلاح کنند. این تغییرات در اجرای دکوراتورهای TypeScript یک مزیت برای اکوسیستم گسترده تر است، اما پذیرش جامعه ممکن است مدتی طول بکشد.