ساخت نوع های فرعی مبتنی بر شرط در تایپ اسکریپت — به زبان ساده
در این مقاله قصد داریم نوع های فرعی مبتنی بر شرط در تایپ اسکریپت و همچنین انواع «نگاشت» (Mapping) را مورد بررسی قرار دهیم. هدف ما این است که نوعی ایجاد کنیم که همه کلیدهایی را که با شرط خاصی مطابقت ندارند را از اینترفیس فیلتر کند.
به این منظور نیازی به آشنایی با جزییات نوعهای نگاشتی وجود ندارد. کافی است بدانید که تایپاسکریپت امکان انتخاب یک نوع موجود و ایجاد تغییر اندک در آن برای ایجاد نوع جدید را به ما میدهد. این بخشی از «کامل بودن تورینگ» (Turing Completeness) آن است.
نوع را میتوان به صورت تابع در نظر گرفت. هر نوع یک نوع دیگر را به عنوان ورودی میگیرد، روی آن نوعی محاسبات اجرا میکند و نوع جدیدی به عنوان خروجی تولید میکند. اگر با <Partial<Type یا <Pick<Type, Keys آشنا هستید، این فرایند نیز کاملاً شبیه آن است.
بیان مسئله
فرض کنید یک شیء پیکربندی دارید. این شیء شامل گروههای مختلفی از کلیدها مانند Ids ،Dates و functions است. این شیء میتواند ناشی از یک API باشد و یا از سوی افراد مختلف سالها نگهداری شود تا این که کاملاً بزرگ شده باشد.
ما میخواهیم تنها کلیدهایی را از نوع خاص استخراج کنیم. این فرایند شبیه به این است که تنها تابعهایی که Promise بازگشت میدهند را انتخاب کنیم یا چیزی سادهتر مانند کلید از نوع number داشته باشیم.
ما به یک نام و تعریف نیاز داریم و از این رو از <SubType<Base, Condition استفاده میکنیم.
بدین ترتیب دو ژنریک تعریف کردهایم که به وسیله آن SubType را پیکربندی خواهیم کرد:
- Base – اینترفیسی است که قرار است آن را تغییر دهیم.
- Condition – نوع دیگری است که به ما اعلام میکند کدام مشخصهها باید در شیء جدید حفظ شوند.
ورودی
به منظور تست کردن یک شیء Person داریم که از نوعهای متفاوتی به صورت string ،number ،Function تشکیل یافته است. این همان «شیء عظیم» ما است که میخواهیم فیلتر کنیم.
1interface Person {
2 id: number;
3 name: string;
4 lastName: string;
5 load: () => Promise<Person>;
6}
خروجی مورد انتظار
برای مثال SubType به نام Person بر اساس نوع رشته میتواند تنها کلیدهایی از نوع رشته را بازگشت دهد:
1// SubType<Person, string>
2
3type SubType = {
4 name: string;
5 lastName: string;
6}
گام به گام به سوی راهحل
در این بخش برای مسئلهای که در بخش قبل مطرح شد، یک راهحل را به صورت گام به گام مطرح میکنیم.
گام 1: Baseline
بزرگترین مشکل، یافتن و حذف کلیدهایی است که با شرط ما مطابقت ندارند. خوشبختانه تایپاسکریپت 2.8 دارای امکانی به نام نوعهای شرطی است. ما به عنوان یک ترفند کوچک قصد داریم نوع پشتیبانی برای یک محاسبه آتی بسازیم.
1type FilterFlags<Base, Condition> = {
2 [Key in keyof Base]:
3 Base[Key] extends Condition ? Key : never
4};
برای هر کلید یک شرط استفاده شده است. بسته به نتیجه نام را به صورت نوع تعیین میکنیم یا از مقدار never استفاده میکنیم که فلگی برای کلیدهایی است که نمیخواهیم در نوع جدید ظاهر شوند. این یک نوع خاص و متضاد any است. به این نوع هیچ چیز نمیتوان انتساب داد.
در ادامه به روش ارزیابی کد میپردازیم:
1FilterFlags<Person, string>; // Step 1
2FilterFlags<Person, string> = { // Step 2
3 id: number extends string ? 'id' : never;
4 name: string extends string ? 'name' : never;
5 lastName: string extends string ? 'lastName' : never;
6 load: () => Promise<Person> extends string ? 'load' : never;
7}
8FilterFlags<Person, string> = { // Step 3
9 id: never;
10 name: 'name';
11 lastName: 'lastName';
12 load: never;
13}
توجه کنید که 'id' یک مقدار نیست، اما نسخه دقیقتری از نوع string محسوب میشود. ما قصد داریم از آن در ادامه استفاده کنیم. تفاوت بین نوع string و 'id' به صورت زیر است:
1const text: string = 'name' // OK
2const text: 'id' = 'name' // ERR
گام 2: فهرستبندی کلیدهایی که با شرط نوع مطابقت دارند
در این مرحله کار اصلی انجام یافته است. اکنون هدف جدیدی داریم و آن گردآوری نامهای کلیدهایی است که از اعتبارسنجی ما عبور کردهاند. در مورد <SubType<Person, string این مقدار برابر با 'name' | 'lastName' خواهد بود:
1type AllowedNames<Base, Condition> = FilterFlags<Base, Condition>[keyof Base]
ما از کد مرحله قبلی استفاده کرده و تنها یک بخش دیگر به نام [keyof Base] به آن اضافه میکنیم.
کار این بخش آن است که رایجترین انواع مشخصههای مفروض را گردآوری کرده و never را نادیده بگیرد چون هیچ راهی برای استفاده از آنها وجود ندارد.
1type family = {
2 type: string;
3 sad: never;
4 members: number;
5 friend: 'Lucy';
6}
7family['type' | 'members'] // string | number
8family['sad' | 'members'] // number (never is ignored)
9family['sad' | 'friend'] // 'Lucy'
در کد فوق مثالی از بازگشت string | number داریم. چنان که میبینید در نخستین گام نوع کلید را با نام آن عوض کردهایم.
1type FilterFlags = {
2 name: 'name';
3 lastName: 'lastName';
4 id: never;
5}
6AllowedNames<FilterFlags, string>; // 'name' | 'lastName'
اینک به راهحل نهایی نزدیک شدهایم.
ما اکنون آماده ساخت شیء نهایی خود هستیم. صرفاً از Pick استفاده میکنیم که روی نامهای کلید ارائه شده میچرخد و نوع متناظر با شیء جدید را استخراج میکند.
1type SubType<Base, Condition> =
2 Pick<Base, AllowedNames<Base, Condition>>
در کد فوق Pick یک نوع نگاشت درونی است که از تایپاسکریپت 2.1 به بعد ارائه شده است:
1Pick<Person, 'id' | 'name'>;
2// equals to:
3{
4 id: number;
5 name: string;
6}
راهحل نهایی
اگر بخواهیم همه گامهای فوق را جمعبندی کنیم باید بگوییم که ما دو نوع ایجاد کردهایم که از پیادهسازی SubType ما پشتیبانی میکنند:
1type FilterFlags<Base, Condition> = {
2 [Key in keyof Base]:
3 Base[Key] extends Condition ? Key : never
4};
5type AllowedNames<Base, Condition> =
6 FilterFlags<Base, Condition>[keyof Base];
7type SubType<Base, Condition> =
8 Pick<Base, AllowedNames<Base, Condition>>;
نکته: این صرفاً یک سیستم نوعبندی است. میتوان در آن از حلقه و حتی اعمال گزارههای شرطی نیز استفاده کرد. برخی افراد ترجیح میدهند که درون یک عبارت نوعهایی داشته باشند:
1type SubType<Base, Condition> = Pick<Base, {
2 [Key in keyof Base]: Base[Key] extends Condition ? Key : never
3}[keyof Base]>;
کاربرد
کاربردهای مختلفی که این نوعهای شرطی دارند به شرح زیر هستند:
۱. استخراج صرف انواع کلید primitive از JSON:
1type JsonPrimitive = SubType<Person, number | string>;
2// equals to:
3type JsonPrimitive = {
4 id: number;
5 name: string;
6 lastName: string;
7}
8// Let's assume Person has additional address key
9type JsonComplex = SubType<Person, object>;
10// equals to:
11type JsonComplex = {
12 address: {
13 street: string;
14 nr: number;
15 };
16}
2. فیلتر کردن همه چیز به جز تابعها:
1interface PersonLoader {
2 loadAmountOfPeople: () => number;
3 loadPeople: (city: string) => Person[];
4 url: string;
5}
6type Callable = SubType<PersonLoader, (_: any) => any>
7// equals to:
8type Callable = {
9 loadAmountOfPeople: () => number;
10 loadPeople: (city: string) => Person[];
11}
شما میتوانید به کاربردهای دیگر این امکان نیز فکر کنید.
این راهحل چه مسائلی را حل نمیکند؟
۱. یک سناریوی جالب این است که یک زیرنوع Nullable بسازیم
اما از آنجا که string | null نمیتواند به null انتساب پیدا کند کار نخواهد کرد. البته ممکن است شما ایدهای برای حل این مشکل داشته باشید!
1// expected: Nullable = { city, street }
2// actual: Nullable = {}
3type Nullable = SubType<{
4 street: string | null;
5 city: string | null;
6 id: string;
7}, null>
2. فیلترینگ زمان اجرا
چنان که میدانید نوعها در زمان کامپایل پاک میشوند. بدین ترتیب هیچ نقشی در شیء واقعی ندارند. اگر بخواهید یک شیء را به نوعی فیلتر کنید، باید کد جاوا اسکریپت برای آن بنویسید.
ضمناً استفاده از ()Object.keys روی چنین ساختارهایی توصیه نمیشود، زیرا نتیجه «زمان اجرا» ممکن است از نوع مفروض متفاوت باشد.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- راهنمای جامع تایپ اسکریپت (Typescript) — از صفر تا صد
- پنج ابزار برای توسعه سریع اپلیکیشن های Vue.js — راهنمای کاربردی
- قابلیت های کمتر شناخته شده TypeScript — راهنمای کاربردی
==