تسلط بر Generics در Typescript – DEV Community
ژنریکها یکی از ابرقدرتهای TypeScript هستند، و اگرچه اگر کسی اطلاعات کمی در مورد آنها داشته باشد یا اصلاً اطلاعاتی نداشته باشد، ممکن است ترسناک به نظر برسند، اما اگر به درستی درک و استفاده شوند، میتوانند نجات دهند. تسلط بر ژنریک ها در TypeScript می تواند به طور قابل توجهی توانایی فرد برای نوشتن کدهای تمیز، قوی و مقیاس پذیر را افزایش دهد. در این مقاله، ما اصول اولیه ژنریک ها را بررسی می کنیم، به برخی از مفاهیم آن می پردازیم و مثال های عملی برای کمک به افراد برای تبدیل شدن به کاربران بهتر از ژنریک ها در TypeScript ارائه می دهیم.
Generics در Typescript چیست؟
Generics ما را قادر می سازد تا اجزای قابل استفاده مجدد ایجاد کنیم که با انواع داده های مختلف کار می کنند و در عین حال ایمنی نوع را حفظ می کنند. آنها راهی برای تعریف متغیرهایی برای انواعی که هنگام استفاده از کد تعیین می شوند، ارائه می دهند. این به ما امکان می دهد کدهای عمومی و انعطاف پذیرتری بنویسیم.
این مثال از این تابع را در زیر در نظر بگیرید:
function echo(value) {
return value;
}
//Call the function
echo("Hello World")
این echo
تابع یک مقدار را دریافت می کند و آن مقدار را بدون هیچ تغییری برمی گرداند. در حال حاضر، TypeScript به پارامتر تابع ما اخم می کند و شکایت می کند که پارامتر ‘value’ به طور ضمنی دارای یک نوع ‘any’ است. به این معنی که از آنجایی که هیچ نوع صریحاً مشخص نشده است، TypeScript یک نوع از را استنباط کرده است any
به پارامتر تابع این به ما کمک نمیکند دادههای پارامتر تابع خود را بررسی کنیم، پس چگونه بررسی کنیم که وقتی تابع ما فراخوانی میشود نوع مناسب ارسال شده است و اینکه تابع ما نیز دادههای صحیح را برمیگرداند؟
یکی از راه هایی که می توانیم این کار را انجام دهیم، تنظیم صریح نوع پارامتر است value
گذشت و برگشت
function echo(value:string):string {
return value;
}
echo("Hello World")
این مثال کاملاً کار می کند زیرا ما می دانیم نوع داده ای که به عنوان پارامتر ارسال می شود و داده هایی که تابع ما برمی گرداند، اما یک مشکل کوچک وجود دارد. اگر بخواهیم آن را بسازیم چه می شود echo
تابع قابل استفاده مجدد است و آن را در جای دیگری با یک نوع داده متفاوت فراخوانی کنید؟ تایپ اسکریپت شکایت می کند که نوع داده اشتباهی را به آن ارسال می کند echo
تابع.
function echo(value:string): string {
return value;
}
//Argument of type 'number' is not assignable to parameter of type 'string'
echo(3.14159)
پس چگونه این را برطرف کنیم؟ چگونه یک تابع قابل استفاده مجدد ایجاد کنیم که انواعی را که به آن ارسال می کنیم را می پذیرد و برمی گرداند؟ ژنریک ها!
اصول عمومی
شروع با اصول اولیه، با استفاده از echo
به عنوان مثال، اجازه می دهد تا آن را ادویه کنید و عملکرد را با ژنریک به روز کنید تا قابل استفاده مجدد باشد.
function echo<T> (value: T): T {
return value;
}
//calls the function with no errors from typescript
echo("Hello World")
echo(3.14159);
echo(false);
echo([1, 2, 3, 4])
echo({id: 1, fullName: "John Doe"})
در این مثال، تابع echo یک تابع عمومی است که یک آرگومان می گیرد value
از نوع T
و مقدار دقیق نوع را برمی گرداند T
بدون هیچ تغییری پارامتر نوع عمومی <T>
ما به تابعی منتقل شدیم که به آن اجازه می دهد با انواع مختلف کار کند، بنابراین به ما امکان می دهد از همان تابع با داده های مختلف دوباره استفاده کنیم.
به طور کلی، شما می توانید یک نوع عمومی با هر الفبای دلخواه بنویسید، <X>,
<Y>
، ولی T
است برای type
، و این فقط یک سنت نامگذاری ژنریک است، و هیچ چیزی مانع از استفاده شما از نام ها یا الفبای دیگر نمی شود.
ژنریک در اعلان های نوع
ژنریک ها فقط با آن کار نمی کنند functions
; همچنین میتوانیم از ژنریکها برای قویتر کردن، استفاده مجدد و انعطافپذیرتر کردن انواع در هنگام اعلام استفاده کنیم. به عنوان مثال، فرض کنید در حال ساخت یک برنامه تجارت الکترونیکی هستیم و یک رابط مخزن داریم که عملیات مشترک CRUD را برای کار با موجودیت های مختلف در سیستم خود تعریف می کند. به جای ایجاد رابط های جداگانه برای هر موجودیت (به عنوان مثال، UserRepository
، ProductRepository
و غیره)، میتوانیم از ژنریک برای ایجاد یک تک، عمومی استفاده کنیم Repository
رابطی که می تواند انواع مختلف موجودیت را مدیریت کند.
interface Repository<T> {
getById(id: string): T | undefined;
getAll(): T[];
create(item: T): void;
update(item: T): void;
delete(id: string): void;
}
class UserRepository implements Repository<User> {
// Implementation specific to User entity
}
class ProductRepository implements Repository<Product> {
// Implementation specific to Products operation
}
// Usage
const userRepository: Repository<User> = new UserRepository();
const user = userRepository.getById('123');
userRepository.create(newUser);
const productRepository: Repository<Product> = new ProductRepository();
const products = productRepository.getAll();
productRepository.update(updatedProduct);
در این مثال، Repository
رابط با یک پارامتر نوع عمومی تعریف شده است T
نشان دهنده نوع موجودیت روش های رایجی مانند getById
، getAll
، create
، update
، و delete
که با هر موجودیتی قابل استفاده است.
با اجرای Repository
رابط با انواع موجودیت خاص مانند User
و Product
، می توانیم مخازن تخصصی ایجاد کنیم که عملیات خاص آن موجودیت ها را مدیریت می کند. ماهیت عمومی اینترفیس امکان استفاده مجدد از کد و انعطاف پذیری هنگام کار با انواع مختلف موجودیت ها را فراهم می کند.
این رویکرد کد را ماژولارتر، قابل نگهداری و توسعهپذیرتر میکند، زیرا ما به راحتی میتوانیم مخازن جدید ویژه موجودیت را بدون کپی کردن مجموعهای از روشها برای هر نوع موجودیت اضافه کنیم.
ژنریک در توابع با محدودیت نوع
همانطور که از اولین مثال ژنریک خود در این مقاله دیدیم، میتوانیم از ژنریکها با توابع استفاده کنیم و آنها را با برخی از ویژگیهای دیگر TypeScript ترکیب کنیم تا نتایج هیجانانگیزتری تولید کنیم. فرض کنید می خواهیم تابعی فراخوانی شود printLength
که فقط موارد با طول را می پذیرد و طول هر اقلامی را که روی آن ارسال می کنیم چاپ می کند. با استفاده از extend
کلمه کلیدی در Typescript، ما می توانیم تابعی ایجاد کنیم که داده های ارسال شده به تابع را محدود کند.
interface Lengthy {
length: number;
}
function printLength<T extends Lengthy>(item: T): void {
console.log(`Length: ${item.length}`);
}
const stringItem = "Hello, World!";
printLength(stringItem); // Output: Length: 13
const arrayItem = [1, 2, 3, 4, 5];
printLength(arrayItem); // Output: Length: 5
const numberItem = 42; // Error: Type 'number' does not have a property 'length'
printLength(numberItem);
در این مثال، The printLength
تابع یک پارامتر نوع عمومی می گیرد T
که گسترش می دهد Lengthy
رابط با استفاده از extends
کلمه کلیدی. این یعنی نوع T
باید داشته باشد length
خاصیت نوع number
.
در داخل printLength
تابع، ما می توانیم با خیال راحت به length
دارایی از item
استدلال بدون ایجاد خطاهای نوع به دلیل نوع عمومی T
تضمین شده است که یک length
ویژگی.
با استفاده از printLength
عملکرد با آیتم ها (stringItem
و arrayItem
) که راضی می کند Lengthy
الزامات، ما هیچ خطایی از TypeScript دریافت نکردیم. با این حال، وقتی سعی می کنیم یک عدد را پاس کنیم (numberItem
) که الف ندارد length
ویژگی، TypeScript یک خطا ایجاد می کند.
بیایید مثال دیگری را در نظر بگیریم که ژنریک ها را با برخی از ویژگی های داخلی دیگر TypeScript ترکیب می کند. تابع زیر آرایه ای از اشیاء را می پذیرد، یک شی را با استفاده از id
ویژگی کلیدی شیء، و مقدار را برمی گرداند key
مقداری که در هنگام فراخوانی تابع به عنوان پارامتر تعیین کردیم.
interface Person {
id: number;
name: string;
age: number;
}
function getObjectValue<T extends { id: number }, K extends keyof T>
(arr: T[], key: K, id: number): T[K] | undefined {
const foundObject = arr.find((obj) => obj.id === id);
return foundObject ? foundObject[key] : undefined;
}
const people: Person[] = [
{ id: 1, name: "John", age: 25 },
{ id: 2, name: "Jane", age: 30 },
{ id: 3, name: "Bob", age: 40 },
];
const nameValue = getObjectValue(people, "name", 2);
console.log(nameValue); // Output: Jane
اوف به نظر می رسد یک عملکرد پیچیده با انواع مختلفی که در همه جا پرواز می کنند. بیایید آن را ذره ذره تجزیه کنیم.
خط اول تابع شامل دو نوع عمومی است
<T, K>
جایی کهT
یک شی است و دارای محدودیتی است که باید دارای ویژگی شی باشد{id: number }
وK
یک ویژگی کلیدی شی استT
(با استفاده ازkeyof
عملگر) و دارای یک محدودیت است که فقط باید دارای ویژگی هایی از آن باشدT
شی (با استفاده ازextend
کلمه کلیدی).خط دوم شامل پارامترهایی است که تابع دریافت می کند و آنچه تابع برمی گرداند.
arr
: آرایه ای از شی ماستT
.key
: یک ویژگی کلیدی شی استT
.id
: یک عدد منحصر به فرد برای هر شی استT
در آرایه- سپس تابع getObjectValue مقدار ویژگی کلید شی پیدا شده، T را برمی گرداند[K]، یا اگر مقدار پیدا نشد تعریف نشده باشد.
خط سوم با استفاده از آرایه را بررسی می کند
id
به عنوان یک معیار، یافتن شیء با همانid.
خط چهارم بررسی می کند که آیا شی
foundObject
ما فقط وجود واقعی را جستجو کردیم و اگر وجود داشت، با استفاده از کلید آن مقداری را از آن استخراج می کنیمfoundObject[key]
و آن را برگردانید یا برگردانیدundefined
اگر پیدا نشد
خودشه. ما فقط شکستیم getObjectValue
برای درک بهتر تعاریف نوع عمومی که دارد، به قطعات کوچک تبدیل شود.
ژنریک ها می توانند مقادیر پیش فرض داشته باشند.
هنگام کار با توابع نیز می توانیم از ژنریک با مقادیر پیش فرض استفاده کنیم. در قبلی ما getObjectValue
تابع، تصور کنید wed می خواهیم یک پارامتر پیش فرض برای خود اضافه کنیم id
پارامتر به عنوان مقدار پیشفرض تابع ما را بر اساس فیلتر میکند. ما می توانیم تابع را به صورت زیر بازنویسی کنیم:
function getObjectValue<T extends { id: number }, K extends keyof T>
(arr: T[], key:K, id:number = 2): T[K] | undefined {
const foundObject = arr.find((obj) => obj.id === id);
return foundObject ? foundObject[key] : undefined;
}
با به روز رسانی id: number
بودن id: number = 2,
مقدار پیش فرض را بر روی آن قرار می دهیم 2
هر زمان که تابع ما اجرا شود.
نتیجه
در پایان، کلیات را در TypeScript پوشش دادیم، اینکه چگونه کد ما را قابل استفاده مجدد میکنند و چگونه با استفاده از نمونههایی از اعلانهای نوع و توابع به انعطافپذیری به کد ما کمک میکنند.
Generics یکی از ویژگی های TypeScript است که ممکن است در ابتدا بسیار زیاد به نظر برسد، اما زمانی که آن را بهتر بشناسید، متوجه خواهید شد که چگونه کد شما را دقیق تر تایپ می کند.
امیدوارم این مقاله به شما در درک بهتر اصول عمومی کمک کند.
با تشکر برای خواندن. به سلامتی.
منابع
من دوست دارم با شما در ارتباط باشم توییتر | لینکدین | GitHub