Genericها ابزار قدرتمندی هستند که میتوانند به ما در ایجاد توابع با قابلیت استفاده مجدد کمک کنند. در تایپ اسکریپت، متغیرها و سایر ساختارهای داده را میتوانیم به عنوان یک تایپ خاص مانند یک آبجکت، یک تایپ Boolean
یا یک string
تعریف کنیم. با استفاده از generic در تایپ اسکریپت، میتوانیم انواع مختلفی از این متغیرها را که به یک تابع منتقل میشوند، مدیریت نماییم.
Genericها به ما این اجازه را میدهند تا یک پارامتر تایپ را بین <>
، مانند <T>
تعریف کنیم. آنها همچنین این امکان را برای ما فراهم میکنند تا بتوانیم کلاسها، متدها و توابع generic بنویسیم.
ما در این مقاله قصد داریم تا به بررسی استفاده از generic در تایپ اسکریپت بپردازیم و با نحوه استفاده از آن در توابع، کلاسها و interfaceها بیشتر آشنا شویم.
Generic در تایپ اسکریپت روشی برای ایجاد کامپوننتها یا توابع با قابلیت استفاده مجدد است که میتواند چندین تایپ را مدیریت کند. Genericها به ما اجازه میدهند تا ساختارهای داده را بدون تعیین زمان مشخص برای اجرا و در زمان کامپایل بسازیم.
در تایپ اسکریپت، genericها همان هدف، یعنی نوشتن کدهای قابل استفاده مجدد و ایمن که تایپ متغیر در زمان کامپایل مشخص میشود را دنبال میکنند. این بدان معنی است که ما میتوانیم به صورت داینامیک تایپ پارامتر یا تابعی که از قبل تعریف میشود را تعیین کنیم. این موضوع زمانی مفید است که ما نیاز به استفاده از یک منطق خاص در برنامه خود داشته باشیم. با این قطعات منطقی قابل استفاده مجدد، میتوانیم توابعی ایجاد کنیم که تایپهای خاص خود را میگیرند و آنها ارائه میدهند.
ما میتوانیم از genericها برای پیادهسازی بررسیها در زمان کامپایل، حذف type castingها و اجرای توابع generic اضافی در برنامه خود استفاده کنیم. بدون generic، کد برنامه ما در برخی موارد کامپایل میشود، اما ممکن است نتایج مورد انتظار را به دست نیاوریم. همین موضوع میتواند باعث ایجاد مشکلات زیادی شود.
همچنین، با استفاده از genericها میتوانیم تایپها را parameterize کنیم. این ویژگی قدرتمند میتواند به ما در ایجاد کلاسها، interfaceها و توابع قابل استفاده مجدد، تعمیمیافته و type-safe کمک کند.
در مثال زیر، یک تابع ساده داریم که ویژگیهای جدیدی را به آرایهای از آبجکتها اضافه میکند. ما یک interface برای آبجکت خود تعریف کردهایم که یک id
و یک pet
را میگیرد:
interface MyObject { id: number; pet: string; } const myArray: MyObject[] = [ { id: 1, pet: "dog" }, { id: 2, pet: "cat" }, ]; const newPropertyKey = "checkup"; const newPropertyValue: string = '2023-12-03'; const newPropertyAddition = myArray.map((obj) => ({ ...obj, [newPropertyKey]: newPropertyValue, })); console.log(newPropertyAddition);
همانطور که میبینیم، قبلاً چند محدودیت را در داخل کد خود تعریف کردهایم. فرض کنید یک ویژگی داریم که یک مقدار string را میپذیرد. اکنون قصد داریم تا یک ویژگی جدید اضافه کنیم که یک عدد را هم قبول میکند. در این شرایط، اگر بتوانیم به جای ساختن یک تابع دیگر، از تابع اصلی دوباره استفاده کنیم، عالی خواهد بود.
این نقطه همان جایی است که genericهای تایپ اسکریپت مطرح میشوند.
قبل از اینکه راه حل خود را با استفاده از generic بنویسیم، مشکل را بررسی میکنیم. اگر تابع بالا را به آرایهای از stringها منتقل کنیم، خطا زیر را دریافت میکنیم:
'Type ‘number’ is not assignable to type of ‘string’
ما میتوانیم این مشکل را با افزودن any
به تعریف تایپ خود برطرف نماییم:
interface MyObject { id: number; pet: string; } const myArray: MyObject[] = [ { id: 1, pet: "dog" }, { id: 2, pet: "cat" }, ]; const newPropertyKey = "checkup"; const newPropertyValue: any = 20231203; const newPropertyAddition = myArray.map((obj) => ({ ...obj, [newPropertyKey]: newPropertyValue, })); console.log(newPropertyAddition);
با این حال، اگر ما تایپهای دادهای خاصی را در کد خود تعریف نکنیم، استفاده از تایپ اسکریپت ضروری نیست.
این بخش از تابع را برای استفاده از generic تغییر میدهیم:
type MyArray<T> = Array<T>; type AddNewProperty<T> = { [K in keyof T]: T[K]; } & { newProperty: string }; // generic 'newProperty' set to the desired with a name and type interface MyObject { id: number; pet: string; } const myArray: MyArray<MyObject> = [ { id: 1, pet: "dog" }, { id: 2, pet: "cat" }, ]; const newPropertyAddition: MyArray<AddNewProperty<MyObject>> = myArray.map((obj) => ({ ...obj, newProperty: "New value", })); console.log(newPropertyAddition);
در این قطعه کد، یک تایپ به نام <T>
داریم که باعث میشود تا کدی که داریم بتواند عمومیتر عمل کند. این تایپ، data typeای که توسط خود تابع دریافت میشود را در خود نگه میدارد. این بدان معناست که تایپ تابع اکنون بر حسب پارامتر تایپ <T>
parameterize شده است.
ابتدا یک تایپ generic که آرایهای از آبجکتها را نشان میدهد تعریف میکنیم، سپس تایپ دیگری به نام AddNewProperty
میسازیم که یک ویژگی جدید به هر آبجکت در آرایه اضافه میکند.
برای بهبود وضوح کد، میتوانیم تابعی ایجاد کنیم که یک generic را به عنوان آرگومان در نظر گرفته و خود یک generic را return میکند. این همان چیزی است که genericها به آن میپردازند، ایجاد یک تابع که میتواند در چندین مکان مورد استفاده قرار بگیرد:
function genericsPassed<T>(arg: T): [T] { console.log(typeof(arg)) return [arg] } /// type of argument passed through generics genericsPassed(3) //Passed a number genericsPassed(new Date()) //Passed a date object genericsPassed(new RegExp("/([A-Z])\w+/g")) //Passed a regex
در این بخش قصد داریم تا استفاده از generic در کلاس را با هم بررسی کنیم.به عنوان مثال:
class MyObject<T> { id: number; pet: string; checkup: T; constructor(id: number, pet: string, additionalProperty: T) { this.id = id; this.pet = pet; this.checkup = additionalProperty; } } const myArray: MyObject<string>[] = [ new MyObject(1, "cat", "false"), new MyObject(2, "dog", "true"), ]; const newPropertyAddition: MyObject<number | boolean>[] = myArray.map((obj) => { return new MyObject(obj.id, obj.pet, obj.id % 2 === 0); }); console.log(newPropertyAddition);
در قطعه کد بالا، ما یک کلاس ساده به نام MyObject
ایجاد کردیم که حاوی متغیری است که آرایهای از id
، pet
و checkup
میباشد. همچنین یک کلاس generic، MyObject<T>
تعریف میکنیم که یک آبجکت با ویژگیهای id
، pet
و یک ویژگی اضافی، additionalProperty
، از تایپ T
را نشان میدهد. constructor مقادیر این ویژگیها را میپذیرد.
Genericها به طور خاص به توابع و کلاسها مرتبط نیستند. ما در تایپ اسکریپت میتوانیم از genericها در داخل یک interface هم استفاده کنیم. generic interfaceها از پارامترهای تایپ به عنوان placeholder برای نشان دادن data typeهای unknown
استفاده میکنند. هنگامی که ما از generic interfaceها استفاده میکنیم، این placeholderها را با تایپهای concrete پر میکنیم و ساختار را بر حسب نیازهایی که داریم سفارشی مینماییم. به عنوان مثال:
const currentlyLoggedIn = (obj: object): object => { let isOnline = true; return {...obj, online: isOnline}; } const user = currentlyLoggedIn({name: 'Ben', email: 'ben@mail.com'}); const currentStatus = user.online
با نوشتن کد بالا، یک خطا دریافت میکنیم که به ما میگوید نمیتوانیم به ویژگی isOnline
از کاربر دسترسی پیدا کنیم:
Property 'isOnline' does not exist on type 'object'.
در درجه اول این خطا به این دلیل است که تابع currentlyLoggedIn
تایپ آبجکت را که از طریق object typeای که به پارامتر اضافه کردهایم، دریافت نمیکند. ما میتوانیم با استفاده از یک generic این مشکل را برطرف نماییم:
const currentlyLoggedIn = <T extends object>(obj: T) => { let isOnline = true; return {...obj, online: isOnline}; } const user = currentlyLoggedIn({name: 'Benny barks', email: 'benny@mail.com'}); user.online = false;
شکل آبجکتی که در حال حاضر در تابع خود با آن سروکار داریم را میتوانیم در interface زیر تعریف کنیم. در این مثال، <T>
پارامتر تایپ است. هنگام استفاده از interface، میتوانیم آن را با هر تایپ معتبر تایپ اسکریپت جایگزین نماییم:
interface User<T> { name: string; email: string; online: boolean; skills: T; } const newUser: User<string[]> = { name: "Benny barks", email: "ben@mail.com", online: false, skills: ["chewing", "barking"], }; const brandNewUser: User<number[]> = { name: "Benny barks", email: "benny@mail.com", online: false, skills: [2456234, 243534], };
در ادامه یک مثال واقعی از نحوه استفاده از یک generic interface را داریم. ما یک interface ILogger
ایجاد کردیم. این interface یک متد log
را تعریف میکند که message و data از هر تایپ (T
) را میگیرد:
interface ILogger<T> { log(message: string, data: T); }
interface ILogger
را میتوانیم با هر data typeای استفاده کنیم و همین موضوع کد ما را با سناریوهای مختلف سازگارتر میکند. همچنین تضمین میکند که دادههای ثبت شده همگی از نوع صحیح هستند.
ابتدا، یک کلاس ConsoleLogger
ایجاد میکنیم که interface ILogger
را پیادهسازی کند:
class ConsoleLogger implements ILogger<any> { log(message: string, data: any) { console.log(`${message}:`, data); } } const user = { name: "John Lee", age: 22 }; const consoleLogger = new ConsoleLogger(); consoleLogger.log("New user added", user);
میتوانیم از ConsoleLogger
برای چاپ پیامها و هر data typeای در کنسول استفاده کنیم.
سپس، میتوانیم یک FileLogger
بسازیم که برای ثبت پیامها در یک فایل، interface ILogger
را پیادهسازی میکند:
class FileLogger implements ILogger<string> { private filename: string; constructor(filename: string) { this.filename = filename; } log(message: string, data: string): void { console.log(`Writing to file: ${this.filename}`); // ... write logEntry to file ... } } const fileLogger = new FileLogger("userlog.txt"); fileLogger.log("User information", JSON.stringify(user));
با استفاده از generic interface ILogger
، میتوانیم یک کلاس concert logger پیادهسازی کنیم که هم مدیریت گزارشگیری از هر data typeای را برای ما انجام میدهد و هم کدی که داریم را انعطافپذیرتر میکند.
ما همچنین میتوانیم یک تایپ عمومی پیشفرض را به generic خود منتقل کنیم. این موضوع زمانی مفید است که نمیخواهیم data typeای که با آن سروکار داریم را به زور در تابع خود منتقل کنیم. در مثال زیر، به طور پیشفرض آن را روی یک تایپ number
تنظیم میکنیم:
function removeRandomArrayItem<T = number>(arr: Array<T>): Array<T> { const randomIndex = Math.floor(Math.random() * arr.length); return arr.splice(randomIndex, 1); } console.log(removeRandomArrayItem([45345, 3453, 356753, 3562345, 3567235]));
این قطعه کد نحوه استفاده از تایپ عمومی پیشفرض را در تابع removeRandomArray
نشان میدهد. با این کار، میتوانیم یک تایپ generic پیشفرض از number
را ارسال نماییم.
اگر میخواهیم بلاکهای توابع قابل استفاده مجدد ما چندین generic داشته باشند، میتوانیم به صورت زیر رفتار کنیم:
function removeRandomAndMultiply<T = number, Y = number>(arr: Array<T>, multiply: Y): [T[], Y] { const randomIndex = Math.floor(Math.random() * arr.length); const multipliedVal = arr.splice(randomIndex, 1); return [multipliedVal, multiply]; } console.log(removeRandomAndMultiply([45345, 3453, 356753, 3562345, 3567235], 608));
در این قطعه کد، ما تابع قبلی خود را برای معرفی یک پارامتر generic دیگر اصلاح کردیم. ما آن را با حرف Y
نشان دادیم، که به صورت پیشفرض بر روی تایپ number
تنظیم شده است. زیرا، بر روی عدد تصادفی که از آرایه داده شده انتخاب کردهایم عملیات ضرب انجام میدهد.
از آنجا که ما در حال ضرب اعداد هستیم، قطعاً با یک تایپ number
سروکار داریم، بنابراین میتوانیم تایپ number
generic پیشفرض را ارسال نماییم.
گاهی اوقات ممکن است بخواهیم تعداد معینی از مقادیری را که یک شرط لازم را قبول میکنند، ارسال کنیم. ما میتوانیم با تعریف یک کلاس با یک پارامتر تایپ generic شرطی مانند زیر این کار را انجام دهیم:
class MyNewClass<T extends { id: number }> { petOwner: T[]; constructor(pets: T[]) { this.petOwner = pets; } processPets<X>(callback: (pet: T) => X): X[] { return this.petOwner.map(callback); } } interface MyObject { id: number; pet: string; } const myArray: MyObject[] = [ { id: 1, pet: "Dog" }, { id: 2, pet: "Cat" }, ]; const myClass = new MyNewClass(myArray); const whichPet = myClass.processPets((item) => { // Add conditional logic based on item properties if (item.pet === 'Dog') { return "You have a dog as a pet!"; } else { return "You have a cat as a pet!"; } }); console.log(whichPet);
در کد بالا، ما یک کلاس تعریف کردهایم، MyNewClass<T extends { id: number }>
، که یک تایپ generic از پارامتر <T>
، که یک آبجکت با ویژگی id
با یک تایپ number
را extend میکند، دریافت مینماید. کلاس یک ویژگی آرایه خالی به نام petOwner
از نوع T
دارد که برای نگهداری آیتمها مورد استفاده قرار میگیرد.
متد processPets
از MyNewClass
یک callback را میپذیرد که شرایط تعریف شده را برای هر کدام از آیتمها بررسی میکند. مقدار بازگشتی whichPet
آرایهای از مقادیر بر اساس شرایط ارائه شده در تابع callback خواهد بود.
Genericها این امکان را به ما میدهند تا بتوانیم با هر data typeای که به عنوان آرگومان ارسال میشود کار کنیم. با این حال، میتوانیم محدودیتهایی را به genericای که داریم اضافه کنیم تا آن را به یک تایپ خاص محدود نماییم.
ما میتوانیم یک پارامتر تایپ را به عنوان محدودیت برای یک پارامتر تایپ دیگر تعریف کنیم. این موضوع به ما کمک میکند تا محدودیتهایی را بر روی آبجکت اضافه نماییم و مطمئن باشیم ویژگی که ممکن است وجود نداشته باشد را دریافت نخواهیم کرد:
function getObjProperty<Type, Key extends keyof Type>(obj: Type, key: Key) { return obj[key]; } let x = { name: "Benny barks", address: "New York", phone: 7245624534534, admin: false }; getObjProperty(x, "name"); getObjProperty(x, "admin"); getObjProperty(x, "loggedIn"); //property doesn't exist
در مثال بالا، برای پارامتر دومی که تابع دریافت میکند، یک محدودیت ایجاد کردیم. میتوانیم این تابع را با آرگومانهای مربوطه فراخوانی کنیم و همه چیز هم به درستی کار میکند مگر اینکه نام ویژگی را که در تایپ آبجکت وجود ندارد، با مقدار x
ارسال نماییم. به این ترتیب میتوانیم ویژگیهای تعریف آبجکت را با استفاده از generic محدود کنیم.
Genericها این امکان را به ما میدهند تا توابع و ساختارهای دادهای را با data typeهای مختلف تعریف کنیم و همزمان type safety را نیز حفظ نماییم.
هنگامی که تایپ تا زمان اجرا مشخص نیست، میتوانیم توابع را با تایپهای generic تعریف کنیم. این تایپهای generic در زمان اجرا با تایپهای خاصی جایگزین خواهند شد. ارسال پارامترهای تایپ generic میتواند به ما کمک کند تا بتوانیم آرایهها با data typeهای مختلف را پردازش کنیم، دادههای JSON را از حالت serialize خارج نماییم، یا این که دادههای response HTTP داینامیک را پردازش کنیم.
فرض کنید در حال ساخت یک برنامه وب هستیم که با یک API تعامل دارد. ما باید یک سرویس گیرنده API ایجاد کنیم که بتواند responseهای API مختلف را با ساختارهای دادهای مختلف مدیریت کند. برای این کار، میتوانیم یک سرویس API را به صورت زیر تعریف کنیم:
interface ApiResponse<T> { data: T; } class ApiService { private readonly baseUrl: string; constructor(baseUrl: string) { this.baseUrl = baseUrl; } public async get<T>(url: string): Promise<ApiResponse<T>> { const response = await fetch(`${this.baseUrl}${url}`); const data = await response.json() as T; return { data }; } }
در این مثال، ما یک interface ApiResponse<T>
تعریف میکنیم که ساختار یک generic API response را نشان میدهد. این interface شامل یک ویژگی data
از نوع T
است و میتواند با ویژگیهای دیگر، مانند status و پیامهای خطا، extend شود. سپس، یک کلاس ApiService
میسازیم که شامل یک تابع generic است که یک مسیر URL را میگیرد و یک Promise از ApiResponse<T>
را return میکند. این تابع دادهها را از URL ارائه شده دریافت میکند، آنها را parse کرده و پاسخ JSON (دادهها به صورت T
) را return میکند.
با تایپ generic، کلاس ApiService
برای endpointهای APIهای مختلف با تغییر پارامتر تایپ T
در تابع get
قابل استفاده مجدد است. همانطور که در مثال زیر میبینیم، میتوانیم از همان apiClient
برای فراخوانی دو endpoint زیر برای دریافت مشتری و محصولات استفاده کنیم:
interface Client { } interface Product { } async function getClient(id: number): Promise<Client> { const response = await apiClient.get<Client>(`/clients/${id}`); return response.data; } async function getProducts(): Promise<Product[]> { const response = await apiClient.get<Product[]>("/products"); return response.data; }
Genericهای تایپ اسکریپت ابزار قدرتمندی هستند، اما برای این که بتوانیم از آنها در پایگاههای کد بزرگ استفاده کنیم باید درک درستی از best practiceها داشته باشیم.
هنگام کار با genericها، بهتر است برای وضوح بیشتر کد از نامهای توصیفی استفاده کنیم. هنگامی که interfaceها یا توابع generic را تعریف میکنیم، پارامترهای تایپ واضح و توصیفی را باید در اولویت بالاتر قرار دهیم. بهتر است از نامهایی استفاده کنیم که به طور دقیق data typeهای مورد انتظار ما را منعکس کنند.
به عنوان مثال، ما یک doubleValue<T>
در ادامه تعریف میکنیم. این تابع generic تایپ و هدف مورد انتظار تابع را بیان میکند و کدی که داریم را خواناتر کرده و قابلیت نگهداریتر آن را بالاتر میبرد:
function doubleValue<T extends number>(value: T): T { return value * 2; }
علاوه بر این، در صورت لزوم باید محدودیتهایی را اعمال کنیم. میتوانیم از محدودیتهای تایپ (کلمه کلیدی extends
) برای محدود کردن تایپهایی که میتوان با genericها استفاده کرد، استفاده نماییم و اطمینان حاصل کنیم که فقط تایپهای سازگار در کد ما پذیرفته میشوند.
در مثال زیر، یک generic interface به عنوان یک محدودیت پارامتر تعریف و اعمال میکنیم، بنابراین تابع findById
فقط آبجکتهایی را میپذیرد که interface خاص مورد نظر ما را پیادهسازی کنند:
interface Identifiable<T> { id: T; } function findById<T, U extends Identifiable<T>>(collection: U[], id: T) { return collection.find(item => item.id === id); }
همچنین استفاده از تایپهای utility بسیار مهم است. تایپ اسکریپت تایپهای utility مانند Partial<T>
، Readonly<T>
و Pick<T, K>
را برای تسهیل دستکاری دادههای رایج ارائه میدهد. اینها میتوانند کدی که داریم را بهینهتر کرده و خوانایی آن را افزایش دهند:
// Partial<T> creates a type with optional properties type UserPartial = Partial<User>; const userData: UserPartial = { name: "Alice" }; // Only give a subset of properties
هنگام کار با genericهای تایپ اسکریپت، اغلب با مشکلاتی مانند “type is not generic"
مواجه میشویم. عیبیابی این مشکلات نیازمند یک رویکرد سیستماتیک و درک چگونگی کارکرد generic در تایپ اسکریپت است. در زیر لیستی از مشکلات متداول و راهکارهایی برای عیبیابی آنها را با هم بررسی خواهیم کرد.
“Type is not generic"
/"Generic type requires type argument"
این خطا اغلب زمانی رخ میدهد که از یک تایپ generic بدون ارائه آرگومانهای تایپ ضروری استفاده کنیم، یا این که از یک تایپ غیر generic با پارامترهای تایپ استفاده نماییم.
راه حل این است که تایپ المنتهایی که آرایه باید در خود نگه دارد را مشخص کنیم. به عنوان مثال، در قطعه کد زیر راه حل، اضافه کردن یک آرگومان تایپ به عنوان const foo: Array<number> = [1, 2, 3];
است:
interface User { id: number } // Attempt to use User as a generic parameter const user : User<number> = {}; //Type is not generice const foo: Array = [1, 2, 3]; //Generic type 'Array<T>' requires 1 type argument(s).
"Cannot Find Name 'T'"
این خطا معمولاً هنگام استفاده از پارامتر تایپ (T
) که تعریف نشده است یا در scope مورد نظر وجود ندارد، رخ میدهد.
برای رفع این مشکل، باید پارامتر تایپ را به درستی تعریف کنیم و یا این که اشتباهات تایپی در استفاده از آن را بررسی نماییم:
// Attempting to use T as a generic type parameter without declaration function getValue(value: T): T { // Cannot find name 'T'. return value; } // Fixing the error by declaring T as a generic type parameter function getValue<T>(value: T): T { return value; }
در این مقاله، مزایای قابل توجه استفاده از generic برای ایجاد کامپوننتهای قابل استفاده مجدد در تایپ اسکریپت را بررسی کردیم. همچنین یاد گرفتیم که چگونه میتوانیم genericها را برای ایجاد توابع، کلاسها و interfaceها پیادهسازی کنیم. در نهایت، با پیادهسازی generic، میتوانیم با کاهش خطاهای زمان اجرا و بهبود قابلیت نگهداری، کدی که داریم را بهبود بخشیم.
۵۰ درصد تخفیف ویژه پاییز فرانت کست تا پایان هفته
کد تخفیف: atm