کاربرد ژنریک و Augmentation در تایپ اسکریپت — از صفر تا صد
تایپاسکریپت یک زبان عالی است. اگر به تازگی کدنویسی با آن را آغاز کردهاید، احتمالاً از ماهیت آزاد و آسانگیر آن خوشتان میآید، چون هر چه چیزهای بیشتری به آن بدهید، چیزهای بیشتری به دست میآورید. اگر از «حاشیهنویسی نوع» (type annotation) استفاده کنید در پارهای موارد از قابلیتهای تکمیل خودکار و همچنین سرنخهایی از کامپایلر بهرهمند میشوید، اما در اغلب موارد چنین نیست.
به مرور متوجه میشوید که هر کجا کامپایلر را دور زدهاید، باید منتظر بروز یک خطای زمان اجرا باشید. هر بار که از as any برای دور زدن خطاها استفاده کنید، باید بدانید که در ادامه باید ساعتها وقت خود را صرف دیباگ کردن بکنید.
بنابراین به مرور یاد میگیرید که با کامپایلر دوست شوید و در مواردی که به شما چیزی را اطلاع میدهد به آن با دقت توجه کنید. کامپایلر لغزشهای ما را میبیند و پیش از آن که به آسیب واقعی تبدیل شوند به ما هشدار میدهد. شما به عنوان یک توسعهدهنده باید درک کنید که کامپایلر همواره بهترین دوست شما است زیرا از شما در مقابل خودتان محافظت میکند.
هر چند که کامپایلر یک دوست خوب محسوب میشود، اما همواره دوست چندان خوشایندی به حساب نمیآید. اجتناب از any در موارد مختلف ممکن است دشوار باشد و برخی اوقات به نظر میرسد که any تنها راهحل ممکن است. در این مقاله دو مورد از این حالتها را بررسی میکنیم و نشان میدهیم که چگونه باید از any اجتناب کنید. بدین ترتیب کدی به دست میآید که امن از نظر نوع، دارای قابلیت استفاده مجدد و گویا است.
استفاده از ژنریک ها در تایپ اسکریپت
تصور کنید مشغول کار روی یک پایگاه داده مدرسه هستیم و تابع کاربردی زیبایی به نام getBy نوشتهایم.
برای این که دانشآموزی را به وسیله نامش به دست آوریم، میتوانیم کد زیر را اجرا کنیم:
1getBy(model, "name", "Harry")
در ادامه به بررسی این کد میپردازیم. توجه کنید که به منظور سادهسازی مثال مدل پایگاه داده را به صورت یک آرایه ساده پیادهسازی کردهایم.
1type Student = {
2 name: string;
3 age: number;
4 hasScar: boolean;
5};
6
7const students: Student[] = [
8 { name: "Harry", age: 17, hasScar: true },
9 { name: "Ron", age: 17, hasScar: false },
10 { name: "Hermione", age: 16, hasScar: false }
11];
12
13function getBy(model, prop, value) {
14 return model.filter(item => item[prop] === value)[0]
15}
بنابراین اکنون یک تابع زیبا داریم، اما این تابع هیچ حاشیهنویسی نوع ندارد و این مسئله به معنی عدم امنیت نوع است. بنابراین در ادامه این وضعیت را اصلاح میکنیم:
1function getBy(model: Student[], prop: string, value): Student | null {
2 return model.filter(item => item[prop] === value)[0] || null
3}
4
5
6const result = getBy(students, "name", "Hermione") // result: Student
اینک اوضاع بسیار بهتر شد. کامپایلر اکنون نوع نتیجه را میداند و این مسئله در ادامه به ما کمک زیادی خواهد کرد. با این حال، ما برای دستیابی به «امنیت نوع» قابلیت استفاده مجدد تابع را قربانی کردهایم. اگر بخواهیم از آن در ادامه برای بازیابی نهادهای دیگر استفاده کنیم چطور؟ بنابراین باید به دنبال روش بهتری باشیم.
در تایپاسکریپت نیز مانند همه زبانها با نوعبندی قوی دیگر میتوانیم از انواع ژنریک استفاده کنیم. ژنریک مانند یک متغیر است، اما به جای نگهداری یک مقدار شامل تعریف یک نوع است. بدین ترتیب تابع خود را طوری بازسازی میکنیم که از یک نوع ژنریک T به جای Srudent استفاده کند:
1function getBy<T>(model: T[], prop: string, value): T | null {
2 return model.filter(item => item[prop] === value)[0]
3}
4
5const result = getBy<Student>(students, "name", "Hermione") // result: Student
تابع ما در حال حاضر کاملاً قابل استفاده مجدد است و از امنیت نوع نیز برخوردار است. توجه کنید که در خط 5 به صراحت به کامپایلر اطلاع دادهایم که از نوع Student به عنوان یک ژنریک به نام T استفاده میکنیم. این کار به منظور روشن شدن موضوع انجام یافته، اما کامپایلر میتواند در عمل خودش این مسئله را استنباط کند و از این رو در مثالهای بعدی آن را نخواهید دید.
اکنون ما یک تابع کاربردی پایدار و یا قابلیت استفاده مجدد داریم، اما باز هم میتوانیم به روشی بهتر عمل کنیم. اگر یک غلط املایی داشته باشیم و در پارامتر دوم خود چیزی مانند naem بنویسیم چطور؟ بدین ترتیب تابع به صورت خاموشی از کار میافتد. در این صورت سیستم طوری عمل میکند که گویی دانشآموز وجود ندارد و شاید به ساعتها صرف وقت برای دیباگ کردن نیاز داشته باشیم.
برای اصلاح این موضوع یک نوع ژنریک جدید به نام P تعریف میکنیم. ما میخواهیم P کلید نوع T باشد بنابراین در صورتی که از Student استفاده کرده باشیم، میخواهیم P به صورت "name", "age" یا "hasScar" باشد. روش کار به صورت زیر است:
1function getBy<T, P extends keyof T>(model: T[], prop: P, value): T | null {
2 return model.filter(item => item[prop] === value)[0] || null
3}
4
5const result = getBy(students, "naem", "Hermione")
6// Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "age" | "hasScar"'.
استفاده از انواع ژنریک به همراه keyof تکنیکی بسیار قدرتمند است. بدین ترتیب اگر در یک IDE کدنویسی میکنید که از تایپاسکریپت پشتیبانی میکند قابلیت تکمیل خودکار را در زمان وارد کردن آرگومانها خواهید داشت که بسیار کارآمد است.
اما کار ما هنوز پایان نیافته است. همچنان یک آرگومان سوم در تابع وجود دارد که بی نوع است و این موضوع غیر قابل قبول است. تا به این جا ما هیچ راهی برای دانستن این که نوع آن چه چیزی باید باشد نداشتیم چون به مقداری که در پارامتر دوم ارسال میکنیم بستگی دارد. اما اکنونی نوع P را داریم و میتوانیم آن را به صورت دینامیک استنباط کنیم. از این رو نوع این آرگومان سوم [T[P است که T اشاره به Student و P اشاره به age دارد. بنابراین [T[P از نوع number خواهد بود.
1function getBy<T, P extends keyof T>(model: T[], prop: P, value: T[P]): T | null {
2 return model.filter(item => item[prop] === value)[0] || null
3}
4
5const result = getBy(students, "age", "17")
6// Error: Argument of type '"17"' is not assignable to parameter of type 'number'.
7
8const anotherResult = getBy(students, "hasScar", "true")
9// Error: Argument of type '"true"' is not assignable to parameter of type 'boolean'.
10
11const yetAnotherResult = getBy(students, "name", "Harry")
12// That's cool
امیدواریم در این مرحله درک کاملاً روشنتری از کاربرد ژنریک ها داشته باشید، اما اگر میخواهید خودتان امتحان کنید، پا را فرارتر بگذارید و به بررسی نمونه کد کامل که در این صفحه (+) وجود دارد بپردازید.
ارتقای انواع موجود
برخی اوقات، ممکن است لازم باشد دادهها یا کارکردی را به اینترفیس اضافه کنیم که خارج از کنترل است. ممکن است لازم باشد که شیء استانداردی را تغییر دهید، مثلاً یک مشخصه را به شیء window اضافه میکنیم یا رفتار نوعی کتابخانه خارجی مانند Express را ارتقا دهیم. در هر دو حالت، نمیتوانید تعریف نوع اشیایی را که روی آن کار میکنید تغییر دهید. در این حالت، ما تابع خود به نام getBy را به پروتوتایپ آرایه اضافه میکنیم، به طوری که با ساختار روشنتری مواجه شوید. این که این ایده مناسب است یا نه، در این لحظه زیاد مهم نیست زیرا فعلاً قصد یادگیری تکنیک را داریم. زمانی که تلاش میکنیم تابع خود را به پروتوتایپ آرایه اضافه کنیم، میتوانیم ببینیم که کامپایلر به ما اعتراض میکند:
1Array.prototype.getBy = function <T, P extends keyof T>(
2 this: T[],
3 prop: P,
4 value: T[P]
5): T | null {
6 return this.filter(item => item[prop] === value)[0] || null;
7};
8// Error: Property 'getBy' does not exist on type 'any[]'.
9
10const bestie = students.getBy("name", "Ron");
11// Error: Property 'getBy' does not exist on type 'Student[]'.
12
13const potionsTeacher = (teachers as any).getBy("subject", "Potions")
14// No error... but at what cost?
اگر تلاش کنیم که با نوشتن as any در این مرحله کامپایلر را خاموش کنیم، همه چیزهایی که برایشان زحمت کشیده بودیم را از دست میدهیم و هر چند کامپایلر دیگر اعتراض نمیکند، اما دیگر از امنیت نوع برخوردار نخواهیم بود.
رویکرد بهتر این است که نوع آرایه را ارتقا دهیم، اما ابتدا باید در مورد شیوه مدیریت دو اینترفیس با نوع یکسان از سوی تایپاسکریپت صحبت بکنیم. پاسخ ساده است. تایپاسکریپت در صورت امکان تعریفها را ادغام میکند و در غیر این صورت خطایی بروز میدهد. بنابراین کد زیر کار میکند:
1interface Wand {
2 length: number
3}
4
5interface Wand {
6 core: string
7}
8
9const myWand: Wand = { length: 11, core: "phoenix feather" }
10// Works great!
و کد زیر کار نمیکند:
1interface Wand {
2 length: number
3}
4
5interface Wand {
6 length: string
7}
8// Error: Subsequent property declarations must have the same type. Property 'length' must be of type 'number', but here has type 'string'.
اکنون که این نکته را درک کردیم، با وظیفه نسبتاً آسانی مواجه هستیم. تنها کاری که باید انجام دهیم این است که یک اینترفیس <Array<T اعلان و آن را به تعریف getBy اضافه کنیم:
1interface Array<T> {
2 getBy<P extends keyof T>(prop: P, value: T[P]): T | null;
3}
4
5Array.prototype.getBy = function <T, P extends keyof T>(
6 this: T[],
7 prop: P,
8 value: T[P]
9): T | null {
10 return this.filter(item => item[prop] === value)[0] || null;
11};
12
13
14const bestie = students.getBy("name", "Ron");
15// Now it works!
16
17const potionsTeacher = (teachers as any).getBy("subject", "Potions")
18// This works as well
نکته مهم: در اغلب موارد احتمالاً در فایلهای ماژول کدنویسی میکنید و از این رو برای تغییر دادن اینترفیس Array باید دسترسی به حیطه global داشته باشید. این وضعیت به سادگی با قرار دادن تعریف نوع درون declare global به صورت زیر امکانپذیر است:
1declare global {
2 interface Array<T> {
3 getBy<P extends keyof T>(prop: P, value: T[P]): T | null;
4 }
5}
اگر قصد دارید یک اینترفیس کتابخانه خارجی را ارتقا دهید، احتمالاً لازم خواهد بود که به namespace کتابخانه دسترسی داشته باشید. در ادامه مثالی از شیوه افزودن یک فیلد userid به request کتابخانه Express را میبینید:
1declare global {
2 namespace Express {
3 interface Request {
4 userId: string;
5 }
6 }
7}
همانند قبل، میتوانید با کد مثال فوق در این صفحه (+) کار کنید تا تسلط بیشتری روی آن بیابید.
سخن پایانی
در این مقاله با موضوعات جدیدی آشنا شدیم که به بهبود کدنویسی ما در زبان تایپاسکریپت کمک زیادی میکنند. ژنریک ها و همچنین بحث «ارتقا» (Augmentation) دو موضوع بسیار مهم در این زبان برنامهنویسی محسوب میشوند و در صورتی که میخواهید به یک برنامهنویس پیشرفته تایپاسکریپت تبدیل شوید، باید روی آنها احاطه خوبی داشته باشید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای طراحی سایت
- راهنمای جامع تایپ اسکریپت (Typescript) — از صفر تا صد
- چگونه برنامه نویس وب شویم؟ – بخش اول: فرانتاند (FrontEnd)
- برنامه های ویرایشگر متن یا IDE، کدامیک برای برنامه نویسان مناسب تر است؟
==