TypeScript زبان برنامه نویسی است که همیشه در حال تکامل پیدا کردن است و ما در این مقاله می خواهیم به ژنریک ها در تایپ اسکریپت نگاهی با هم دیگر بیندازیم.
ژنریک های TypeScript چیست؟
Generics در TypeScript روشی برای ایجاد کامپوننت یا توابع قابل استفاده مجدد است که می تواند چندین تایپ را مدیریت کند. Generics ابزار قدرتمندی است که به ما امکان می دهد کد قابل استفاده مجدد و ایمن در جایی که نوع متغیر در زمان کامپایل مشخص است بنویسیم.
این بدان معنی است که ما می توانیم به صورت پویا نوع پارامتر یا تابعی را که از قبل اعلام می شود تعریف کنیم. این واقعاً زمانی مفید است که ما نیاز به استفاده از منطق خاصی در داخل برنامه خود داریم. با این قطعات منطقی قابل استفاده مجدد، ما می توانیم توابعی ایجاد کنیم که تایپ خود را دریافت و ارسال می کنند.
ما میتوانیم از ژنریکها برای پیادهسازی بررسیها در زمان کامپایل، حذف تایپ castings و اجرای توابع عمومی اضافی در سراسر برنامهمان استفاده کنیم.
ژنریک های TypeScript در عمل
بریم در عمل و کدنویسی بفهمیم که ژنریک ها چطوری کار می کنند.
تابعی که بدون استفاده از ژنریک کار می کند
بیایید مثال زیر را در نظر بگیریم. در زیر، یک تابع ساده داریم که یک شاخص تصادفی را از یک آرایه حذف می کند. نام تابع خود را removeRandomArrayItem
می گذاریم که به شکل زیر است:
function removeRandomArrayItem(arr: Array<number>): Array<number> {
const randomIndex = Math.floor(Math.random() * arr.length);
return arr.splice(randomIndex, 1);
}
removeRandomArrayItem([6, 7, 8, 4, 5, 6, 8, 9]);
تابع بالا به ما می گوید که نام تابع removeRandomArrayItem
است و پارامتری از آیتم را می گیرد که نوعی آرایه متشکل از اعداد است. در نهایت، این تابع یک مقدار را برمی گرداند که آن هم آرایه ای از اعداد است.
همانطور که می بینید، ما قبلاً چند محدودیت را در کد خود معرفی کرده ایم. بیایید بگوییم که میخواهیم به جای آرایه ای از اعداد، آرایه ای از رشتهها را حلقه بزنیم. آیا باید تابع دیگری برای رسیدگی به این مورد استفاده بسازیم؟
نه! اینجا نقطه شیرینی است که ژنریک های TypeScript وارد عمل می شوند.
توابع با استفاده از ژنریک
بیایید قبل از اینکه راه حل خود را با استفاده از ژنریک بنویسیم، به یک مشکل نگاهی بیندازیم. اگر تابع بالا را به آرایه ای از رشته ها تبدیل کنیم، این خطا را برای ما ارسال می کند:
'Type ‘string’ is not assignable to type of ‘number’
ما می توانیم با اضافه کردن هر یک به اعلان تایپ خود این مشکل را برطرف کنیم:
function removeRandomArrayItem(arr: Array<any>): Array<any> {
const randomIndex = Math.floor(Math.random() * arr.length);
return arr.splice(randomIndex, 1);
}
console.log(removeRandomArrayItem(['foo', 1349, 6969, 'bar']));
اما اگر با هیچ نوع داده ای سروکار نداشته باشیم، دلیل معتبری برای استفاده از TypeScript وجود ندارد. بیایید این قطعه کد را با استفاده از ژنریک با استفاده از پارامتر تایپ <T>
باز سازی کنیم:
function removeRandomArrayItem<T>(arr: Array<T>): Array<T> {
const randomIndex = Math.floor(Math.random() * arr.length);
return arr.splice(randomIndex, 1);
}
console.log(removeRandomArrayItem(['foo', 'bar']));
console.log(removeRandomArrayItem([45345, 3453]));
همانطور که در بالا می بینید، ما یک تایپ به نام <T>
را مشخص کردیم که باعث می شود کلی تر عمل کند. این نوع داده هایی را که توسط خود تابع دریافت می شود نگه می دارد.
کلاس های TypeScript با استفاده از ژنریک
بیایید به مثالی از استفاده از ژنریک در کلاس ها نگاهی بیندازیم. موارد زیر را در نظر بگیرید:
class Foo {
items: Array<number> = [];
add(item: number) {
return this.items.push(item);
}
remove(item: Array<number>){
const randomIndex = Math.floor(Math.random() * item.length);
return item.splice(randomIndex, 1);
}
}
const bar = new Foo();
bar.add(22);
bar.remove([1345, 45312613, 13453]);
در اینجا یک کلاس ساده به نام Foo
ایجاد کردیم که حاوی متغیری است که آرایه ای از اعداد است. ما به دو روش ایجاد کردیم: یکی که موارد را به آرایه اضافه می کند و دیگری که یک عنصر تصادفی را از آرایه حذف می کند.
این قطعه کد به خوبی کار میکند، اما ما همان مشکل قبلی را که معرفی کردهایم: اگر مواردی را در آرایه اضافه یا حذف کنیم، عموما فقط یک آرایه از اعداد را میگیرد. بیایید این کلاس را دوباره فاکتور کنیم تا از یک ژنریک برای پذیرش یک مقدار عمومی استفاده کنیم، بنابراین می توانیم هر تایپ را به آرگومان منتقل کنیم:
class Foo<TypeOfFoo> {
items: Array<TypeOfFoo> = [];
add(item: TypeOfFoo) {
return this.items.push(item);
}
remove(item: Array<TypeOfFoo>){
const randomIndex = Math.floor(Math.random() * item.length);
return item.splice(randomIndex, 1);
}
}
const bar = new Foo();
bar.add(22);
bar.add('adfvafdv');
bar.remove([1345, 45312613, 13453]);
bar.remove([1345, 45312613, '13453']);
با استفاده از ژنریک ها در داخل کلاس ها، ما کد خود را بسیار قابل استفاده مجدد و DRY کرده ایم. اینجاست که ژنریک ها واقعا قدرت آن ها را فهمید!
استفاده از ژنریک در interface های TypeScript
ژنریک ها به طور خاص به توابع و کلاس ها مرتبط نیستند. ما همچنین می توانیم از ژنریک در TypeScript در داخل یک interface استفاده کنیم. بیایید به مثالی نگاه کنیم که چگونه می توانیم از آن استفاده کنیم:
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
اگر کد بالا را اجرا کنیم، با یک خط squiggly
خطایی دریافت می کنیم که به ما می گوید نمی توانیم به ویژگی isOnline
از کاربر دسترسی پیدا کنیم:
Property 'isOnline' does not exist on type 'object'.
این در درجه اول به این دلیل است که تابع currentLoggedIn
نوع آبجکت را که از طریق نوع آبجکت ای که به پارامتر اضافه کرده ایم دریافت می کند، نمی داند. ما می توانیم با استفاده از یک ژنریک از این موضوع دور شویم:
const currentlyLoggedIn = <T extends object>(obj: T) => {
let isOnline = true;
return {...obj, online: isOnline};
}
const user = currentlyLoggedIn({name: 'Ben', email: 'ben@mail.com'});
user.online = false;
همانند آبجکتی که در حال حاضر در تابع خود مدیریت می کنیم را می توان در یک interface تعریف کرد:
interface User<T> {
name: string;
email: string;
online: boolean;
skills: T;
}
const newUser: User<string[]> = {
name: "Ben",
email: "ben@mail.com",
online: false,
skills: ["foo", "bar"],
};
const brandNewUser: User<number[]> = {
name: "Ben",
email: "ben@mail.com",
online: false,
skills: [2456234, 243534],
};
بیایید مثال دیگری از نحوه استفاده از یک interface با ژنریک بیاوریم. در زیر، یک interface با Greet
تعریف میکنیم، که یک ژنریک را دریافت میکند، و آن ژنریک خاص، نوعی است که به ویژگی skills
ما منتقل میشود.
با این کار میتوانیم مقدار مورد نظر را به ویژگی skills
در تابع Greet
منتقل کنیم:
interface Greet<T> {
fullName: "Ben Douglass";
skills: T
messageGreet: string
}
const messageGreetings = (obj: Greet<string>): Greet<string> => {
return {
...obj,
messageGreet: `${obj.fullName} welcome to the app`,
skills: 'sd'
};
};
انتقال مقادیر عمومی پیش فرض به ژنریک
ما همچنین می توانیم یک نوع ژنریک پیش فرض را به ژنریک خود منتقل کنیم. این در مواردی مفید است که نمیخواهیم نوع دادهای را که با آن سروکار داریم در عملکرد خود به زور منتقل کنیم. به طور پیش فرض، ما آن را روی یک عدد تنظیم می کنیم.
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
خود استفاده کردیم. با این کار، میتوانیم یک عدد ژنریک پیشفرض را پاس کنیم.
عبور چندین مقدار ژنریک
اگر میخواهیم بلوکهای توابع قابل استفاده مجدد ما چندین ژنریک داشته باشند، میتوانیم کارهای زیر را انجام دهیم:
function removeRandomAndMultiply<T = string, 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));
در اینجا، ما یک نسخه تغییر یافته از تابع قبلی خود ایجاد کردیم تا بتوانیم یک پارامتر ژنریکی دیگر را معرفی کنیم. ما آن را با Y
نشان دادیم، که روی یک نوع عدد پیشفرض تنظیم شده است، زیرا عدد تصادفی را که از آرایه داده شده انتخاب کردهایم ضرب میکند
.
از آنجایی که ما در حال ضرب اعداد هستیم، قطعاً با یک نوع عدد سروکار داریم، بنابراین میتوانیم نوع ژنریک پیشفرض عدد را پاس کنیم.
اضافه کردن محدودیت به ژنریک
Generics به ما اجازه می دهد با هر نوع داده ای که به عنوان آرگومان ارسال می شود کار کنیم. با این حال، میتوانیم محدودیتهایی را به ژنریک اضافه کنیم تا آن را به یک نوع خاص محدود کنیم.
بیایید یک پارامتر تایپ را تعریف کنیم که توسط پارامتر نوع دیگری محدود شده است. این به ما کمک میکند تا محدودیتهایی را به آبجکت اضافه کنیم و اطمینان حاصل کنیم که خاصیتی را که احتمالاً وجود ندارد به دست نمیآوریم:
function getObjProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { name: "Ben", address: "New York", phone: 7245624534534, admin: false };
getObjProperty(x, "name");
getObjProperty(x, "admin");
getObjProperty(x, "loggedIn"); //property doesn't exist
در مثال بالا، برای پارامتر دومی که تابع دریافت میکند، یک محدودیت ایجاد کردیم. میتوانیم این تابع را با آرگومانهای مربوطه فراخوانی کنیم و همه چیز کار میکند، مگر اینکه نام خاصیتی را که در نوع آبجکت وجود ندارد با مقدار x ارسال کنیم. به این ترتیب میتوانیم ویژگیهای تعریف آبجکت را با استفاده از ژنریک محدود کنیم.
نتیجه
در این مقاله ، نحوه استفاده از ژنریک ها را بررسی کردیم و برخی از توابع قابل استفاده مجدد را در کدهای خود ایجاد کردیم. ما ژنریک ها را برای ایجاد یک تابع، کلاس، interfaces، متد، چندین interfaces و ژنریک های پیش فرض پیاده سازی کردیم.
اگر در مورد ژنریک ها سوالی دارید می توانید در قسمت نظرات بپرسید.