فراتر از تایپهای پایه بروید. بر ویژگیهای پیشرفته تایپاسکریپت مانند انواع شرطی، تمپلیت لیترالها و دستکاری رشتهها مسلط شوید تا APIهای فوقالعاده قوی و امن از نظر نوع بسازید. راهنمایی جامع برای توسعهدهندگان جهانی.
آزاد کردن پتانسیل کامل تایپاسکریپت: نگاهی عمیق به انواع شرطی، تمپلیت لیترالها و دستکاری پیشرفته رشتهها
در دنیای توسعه نرمافزار مدرن، تایپاسکریپت بسیار فراتر از نقش اولیهی خود به عنوان یک بررسیکننده نوع ساده برای جاوااسکریپت تکامل یافته است. این ابزار به یک وسیله پیچیده برای چیزی تبدیل شده است که میتوان آن را برنامهنویسی در سطح نوع توصیف کرد. این پارادایم به توسعهدهندگان اجازه میدهد کدی بنویسند که بر روی خودِ انواع (types) عمل میکند و APIهای پویا، خود-مستندساز و فوقالعاده امنی ایجاد میکند. در قلب این تحول، سه ویژگی قدرتمند قرار دارند که با هم کار میکنند: انواع شرطی (Conditional Types)، انواع تمپلیت لیترال (Template Literal Types) و مجموعهای از انواع ذاتی برای دستکاری رشتهها (String Manipulation Types).
برای توسعهدهندگانی در سراسر جهان که به دنبال ارتقای مهارتهای تایپاسکریپت خود هستند، درک این مفاهیم دیگر یک امر تجملی نیست - بلکه برای ساخت برنامههای مقیاسپذیر و قابل نگهداری یک ضرورت است. این راهنما شما را به یک کاوش عمیق میبرد، از اصول بنیادی شروع کرده و به الگوهای پیچیده و واقعی میرسد که قدرت ترکیبی آنها را به نمایش میگذارد. چه در حال ساخت یک سیستم طراحی، یک کلاینت API امن از نظر نوع، یا یک کتابخانه پیچیده برای مدیریت دادهها باشید، تسلط بر این ویژگیها اساساً نحوه نوشتن تایپاسکریپت شما را تغییر خواهد داد.
پایه و اساس: انواع شرطی (سهتایی `extends`)
در هستهی خود، یک نوع شرطی به شما این امکان را میدهد که بر اساس بررسی یک رابطه نوعی، یکی از دو نوع ممکن را انتخاب کنید. اگر با عملگر سهتایی جاوااسکریپت (condition ? valueIfTrue : valueIfFalse) آشنا هستید، سینتکس آن فوراً برایتان قابل درک خواهد بود:
type Result = SomeType extends OtherType ? TrueType : FalseType;
در اینجا، کلمه کلیدی extends به عنوان شرط ما عمل میکند. این کلمه بررسی میکند که آیا SomeType قابل انتساب به OtherType است یا خیر. بیایید با یک مثال ساده آن را تحلیل کنیم.
مثال پایه: بررسی یک نوع
تصور کنید میخواهیم نوعی ایجاد کنیم که اگر نوع داده شده T یک رشته (string) باشد، به true و در غیر این صورت به false تبدیل شود.
type IsString
سپس میتوانیم از این نوع به این صورت استفاده کنیم:
type A = IsString<"hello">; // type A برابر با true است
type B = IsString<123>; // type B برابر با false است
این بلوک ساختمانی اساسی است. اما قدرت واقعی انواع شرطی زمانی آزاد میشود که با کلمه کلیدی infer ترکیب شوند.
قدرت `infer`: استخراج انواع از درون
کلمه کلیدی infer یک تغییردهنده بازی است. این کلمه به شما اجازه میدهد تا یک متغیر نوع جنریک جدید را درون عبارت extends تعریف کنید، و به طور موثر بخشی از نوعی را که در حال بررسی آن هستید، ضبط کنید. به آن به عنوان یک تعریف متغیر در سطح نوع فکر کنید که مقدار خود را از طریق تطبیق الگو دریافت میکند.
یک مثال کلاسیک، باز کردن نوعی است که درون یک Promise قرار دارد.
type UnwrapPromise
بیایید این را تحلیل کنیم:
T extends Promise: این عبارت بررسی میکند که آیاTیکPromiseاست. اگر باشد، تایپاسکریپت سعی میکند ساختار را مطابقت دهد.infer U: اگر تطابق موفقیتآمیز باشد، تایپاسکریپت نوعی را کهPromiseبه آن resolve میشود، ضبط کرده و آن را در یک متغیر نوع جدید به نامUقرار میدهد.? U : T: اگر شرط درست باشد (TیکPromiseبود)، نوع حاصلU(نوع باز شده) خواهد بود. در غیر این صورت، نوع حاصل همان نوع اصلیTاست.
نحوه استفاده:
type User = { id: number; name: string; };
type UserPromise = Promise
type UnwrappedUser = UnwrapPromise
type UnwrappedNumber = UnwrapPromise
این الگو به قدری رایج است که تایپاسکریپت شامل انواع ابزاری داخلی مانند ReturnType است که با استفاده از همین اصل برای استخراج نوع بازگشتی یک تابع پیادهسازی شده است.
انواع شرطی توزیعی: کار با Unionها
یک رفتار جذاب و حیاتی انواع شرطی این است که وقتی نوعی که در حال بررسی است یک پارامتر نوع جنریک «عریان» (naked) باشد، آنها توزیعی (distributive) میشوند. این بدان معناست که اگر یک نوع union به آن بدهید، شرط به طور جداگانه برای هر عضو از union اعمال میشود و نتایج در یک union جدید جمعآوری میشوند.
نوعی را در نظر بگیرید که یک نوع را به آرایهای از آن نوع تبدیل میکند:
type ToArray
اگر یک نوع union را به ToArray بدهیم:
type StrOrNumArray = ToArray
نتیجه (string | number)[] نیست. از آنجا که T یک پارامتر نوع عریان است، شرط توزیع میشود:
ToArrayتبدیل بهstring[]میشودToArrayتبدیل بهnumber[]میشود
نتیجه نهایی، union این نتایج فردی است: string[] | number[].
این خاصیت توزیعی برای فیلتر کردن unionها فوقالعاده مفید است. به عنوان مثال، نوع ابزاری داخلی Extract از این برای انتخاب اعضا از union T که قابل انتساب به U هستند، استفاده میکند.
اگر نیاز به جلوگیری از این رفتار توزیعی دارید، میتوانید پارامتر نوع را در هر دو طرف عبارت extends در یک تاپل (tuple) قرار دهید:
type ToArrayNonDistributive
type StrOrNumArrayUnified = ToArrayNonDistributive
با این پایه محکم، بیایید بررسی کنیم که چگونه میتوانیم انواع رشتهای پویا بسازیم.
ساخت رشتههای پویا در سطح نوع: انواع تمپلیت لیترال
انواع تمپلیت لیترال که در تایپاسکریپت 4.1 معرفی شدند، به شما امکان میدهند انواعی را تعریف کنید که شبیه به رشتههای تمپلیت لیترال جاوااسکریپت هستند. آنها شما را قادر میسازند تا انواع رشتهای لیترال جدید را از انواع موجود الحاق، ترکیب و تولید کنید.
سینتکس آن دقیقاً همان چیزی است که انتظار دارید:
type World = "World";
type Greeting = `Hello, ${World}!`; // type Greeting برابر با "Hello, World!" است
این ممکن است ساده به نظر برسد، اما قدرت آن در ترکیب با unionها و جنریکها نهفته است.
Unionها و جایگشتها
وقتی یک نوع تمپلیت لیترال شامل یک union باشد، به یک union جدید گسترش مییابد که شامل تمام جایگشتهای رشتهای ممکن است. این یک روش قدرتمند برای تولید مجموعهای از ثابتهای به خوبی تعریف شده است.
تصور کنید مجموعهای از خصوصیات margin در CSS را تعریف میکنید:
type Side = "top" | "right" | "bottom" | "left";
type MarginProperty = `margin-${Side}`;
نوع حاصل برای MarginProperty این است:
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
این برای ایجاد پراپهای کامپوننت یا آرگومانهای تابع امن از نظر نوع که فقط فرمتهای رشتهای خاصی مجاز هستند، عالی است.
ترکیب با جنریکها
تمپلیت لیترالها واقعاً زمانی میدرخشند که با جنریکها استفاده شوند. شما میتوانید انواع کارخانهای (factory types) ایجاد کنید که انواع رشتهای لیترال جدید را بر اساس ورودی تولید میکنند.
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
این الگو کلید ایجاد APIهای پویا و امن از نظر نوع است. اما اگر بخواهیم حالت نوشتاری (case) رشته را تغییر دهیم، مانند تغییر `"user"` به `"User"` برای به دست آوردن `"onUserChange"` چه؟ اینجاست که انواع دستکاری رشته وارد میشوند.
جعبه ابزار: انواع ذاتی دستکاری رشته
برای قدرتمندتر کردن تمپلیت لیترالها، تایپاسکریپت مجموعهای از انواع داخلی برای دستکاری لیترالهای رشتهای فراهم میکند. اینها مانند توابع ابزاری هستند اما برای سیستم نوع.
تغییردهندههای حالت نوشتاری: `Uppercase`، `Lowercase`، `Capitalize`، `Uncapitalize`
این چهار نوع دقیقاً همان کاری را انجام میدهند که از نامشان پیداست:
Uppercase: کل نوع رشته را به حروف بزرگ تبدیل میکند.type LOUD = Uppercase<"hello">; // "HELLO"Lowercase: کل نوع رشته را به حروف کوچک تبدیل میکند.type quiet = Lowercase<"WORLD">; // "world"Capitalize: کاراکتر اول نوع رشته را به حرف بزرگ تبدیل میکند.type Proper = Capitalize<"john">; // "John"Uncapitalize: کاراکتر اول نوع رشته را به حرف کوچک تبدیل میکند.type variable = Uncapitalize<"PersonName">; // "personName"
بیایید به مثال قبلی خود برگردیم و آن را با استفاده از Capitalize برای تولید نامهای مرسوم کنترلکننده رویداد (event handler) بهبود دهیم:
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
اکنون ما تمام قطعات را در اختیار داریم. بیایید ببینیم چگونه برای حل مشکلات پیچیده و واقعی با هم ترکیب میشوند.
ترکیب: ادغام هر سه برای الگوهای پیشرفته
اینجاست که تئوری با عمل روبرو میشود. با در هم تنیدن انواع شرطی، تمپلیت لیترالها و دستکاری رشتهها، میتوانیم تعاریف نوع فوقالعاده پیچیده و امنی بسازیم.
الگوی ۱: Event Emitter کاملاً امن از نظر نوع
هدف: ایجاد یک کلاس جنریک EventEmitter با متدهایی مانند on()، off() و emit() که کاملاً امن از نظر نوع باشند. این یعنی:
- نام رویدادی که به متدها ارسال میشود باید یک رویداد معتبر باشد.
- پیلود (payload) ارسال شده به
emit()باید با نوع تعریف شده برای آن رویداد مطابقت داشته باشد. - تابع callback ارسال شده به
on()باید نوع پیلود صحیح برای آن رویداد را بپذیرد.
ابتدا، یک نقشه از نام رویدادها به انواع پیلود آنها تعریف میکنیم:
interface EventMap {
"user:created": { userId: number; name: string; };
"user:deleted": { userId: number; };
"product:added": { productId: string; price: number; };
}
حالا، میتوانیم کلاس جنریک EventEmitter را بسازیم. ما از یک پارامتر جنریک Events استفاده خواهیم کرد که باید ساختار EventMap ما را گسترش دهد.
class TypedEventEmitter
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
// متد `on` از یک جنریک `K` استفاده میکند که کلیدی از نقشه Events ماست
on
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
// متد `emit` تضمین میکند که پیلود با نوع رویداد مطابقت دارد
emit
this.listeners[event]?.forEach(callback => callback(payload));
}
}
بیایید آن را نمونهسازی کرده و استفاده کنیم:
const appEvents = new TypedEventEmitter
// این امن از نظر نوع است. پیلود به درستی به عنوان { userId: number; name: string; } استنتاج میشود
appEvents.on("user:created", (payload) => {
console.log(`User created: ${payload.name} (ID: ${payload.userId})`);
});
// تایپاسکریپت در اینجا خطا میدهد زیرا "user:updated" کلیدی در EventMap نیست
// appEvents.on("user:updated", () => {}); // خطا!
// تایپاسکریپت در اینجا خطا میدهد زیرا پیلود فاقد خصوصیت 'name' است
// appEvents.emit("user:created", { userId: 123 }); // خطا!
این الگو ایمنی در زمان کامپایل را برای بخشی از بسیاری از برنامهها فراهم میکند که به طور سنتی بسیار پویا و مستعد خطا است.
الگوی ۲: دسترسی امن از نظر نوع به مسیر برای اشیاء تودرتو
هدف: ایجاد یک نوع ابزاری، PathValue، که بتواند نوع یک مقدار را در یک شیء تودرتو T با استفاده از یک رشته مسیر با نمادگذاری نقطهای P (مثلاً "user.address.city") تعیین کند.
این یک الگوی بسیار پیشرفته است که انواع شرطی بازگشتی را به نمایش میگذارد.
در اینجا پیادهسازی آن آمده است که آن را تحلیل خواهیم کرد:
type PathValue
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
بیایید منطق آن را با یک مثال ردیابی کنیم: PathValue
- فراخوانی اولیه:
Pبرابر با"a.b.c"است. این با تمپلیت لیترال`${infer Key}.${infer Rest}`مطابقت دارد. Keyبه عنوان"a"استنتاج میشود.Restبه عنوان"b.c"استنتاج میشود.- اولین بازگشت: نوع بررسی میکند که آیا
"a"کلیدی ازMyObjectاست. اگر بله، به صورت بازگشتیPathValueرا فراخوانی میکند. - دومین بازگشت: اکنون،
Pبرابر با"b.c"است. این دوباره با تمپلیت لیترال مطابقت دارد. Keyبه عنوان"b"استنتاج میشود.Restبه عنوان"c"استنتاج میشود.- نوع بررسی میکند که آیا
"b"کلیدی ازMyObject["a"]است و به صورت بازگشتیPathValueرا فراخوانی میکند. - حالت پایه: در نهایت،
Pبرابر با"c"است. این با`${infer Key}.${infer Rest}`مطابقت ندارد. منطق نوع به شرط دوم میرود:P extends keyof T ? T[P] : never. - نوع بررسی میکند که آیا
"c"کلیدی ازMyObject["a"]["b"]است. اگر بله، نتیجهMyObject["a"]["b"]["c"]است. اگر نه،neverاست.
استفاده با یک تابع کمکی:
declare function get
const myObject = {
user: {
name: "Alice",
address: {
city: "Wonderland",
zip: 12345
}
}
};
const city = get(myObject, "user.address.city"); // const city: string
const zip = get(myObject, "user.address.zip"); // const zip: number
const invalid = get(myObject, "user.email"); // const invalid: never
این نوع قدرتمند از خطاهای زمان اجرا ناشی از اشتباهات تایپی در مسیرها جلوگیری میکند و استنتاج نوع کاملی را برای ساختارهای دادهای عمیقاً تودرتو فراهم میکند، که یک چالش رایج در برنامههای جهانی است که با پاسخهای API پیچیده سروکار دارند.
بهترین شیوهها و ملاحظات عملکرد
مانند هر ابزار قدرتمندی، مهم است که از این ویژگیها هوشمندانه استفاده کنید.
- خوانایی را در اولویت قرار دهید: انواع پیچیده میتوانند به سرعت ناخوانا شوند. آنها را به انواع کمکی کوچکتر و با نام خوب تقسیم کنید. برای توضیح منطق، درست مانند کد زمان اجرا، از کامنتها استفاده کنید.
- نوع `never` را درک کنید: نوع
neverابزار اصلی شما برای مدیریت حالتهای خطا و فیلتر کردن unionها در انواع شرطی است. این نوع حالتی را نشان میدهد که هرگز نباید رخ دهد. - مراقب محدودیتهای بازگشت باشید: تایپاسکریپت برای نمونهسازی نوع، یک محدودیت عمق بازگشت دارد. اگر انواع شما بیش از حد عمیق یا به طور نامحدود بازگشتی باشند، کامپایلر خطا میدهد. اطمینان حاصل کنید که انواع بازگشتی شما یک حالت پایه واضح دارند.
- عملکرد IDE را نظارت کنید: انواع بسیار پیچیده گاهی اوقات میتوانند بر عملکرد سرور زبان تایپاسکریپت تأثیر بگذارند و منجر به کندی در تکمیل خودکار و بررسی نوع در ویرایشگر شما شوند. اگر با کندی مواجه شدید، ببینید آیا میتوان یک نوع پیچیده را سادهتر یا تجزیه کرد.
- بدانید چه زمانی باید متوقف شوید: این ویژگیها برای حل مشکلات پیچیده ایمنی نوع و تجربه توسعهدهنده هستند. از آنها برای مهندسی بیش از حد انواع ساده استفاده نکنید. هدف، افزایش وضوح و ایمنی است، نه افزودن پیچیدگی غیرضروری.
نتیجهگیری
انواع شرطی، تمپلیت لیترالها و انواع دستکاری رشته فقط ویژگیهای مجزا نیستند؛ آنها یک سیستم یکپارچه برای انجام منطق پیچیده در سطح نوع هستند. آنها ما را قادر میسازند تا فراتر از حاشیهنویسیهای ساده حرکت کرده و سیستمهایی بسازیم که عمیقاً از ساختار و محدودیتهای خود آگاه هستند.
با تسلط بر این سهگانه، میتوانید:
- APIهای خود-مستندساز ایجاد کنید: خودِ انواع به مستندات تبدیل میشوند و توسعهدهندگان را برای استفاده صحیح از آنها راهنمایی میکنند.
- دستههای کاملی از باگها را حذف کنید: خطاهای نوع در زمان کامپایل شناسایی میشوند، نه توسط کاربران در محیط پروداکشن.
- تجربه توسعهدهنده را بهبود بخشید: از تکمیل خودکار غنی و پیامهای خطای درونخطی حتی برای پویاترین بخشهای کدبیس خود لذت ببرید.
پذیرش این قابلیتهای پیشرفته، تایپاسکریپت را از یک تور ایمنی به یک شریک قدرتمند در توسعه تبدیل میکند. این به شما امکان میدهد منطق پیچیده کسبوکار و ثباتها را مستقیماً در سیستم نوع کدگذاری کنید و اطمینان حاصل کنید که برنامههای شما برای مخاطبان جهانی قویتر، قابل نگهداریتر و مقیاسپذیرتر هستند.