با Mapped Types در TypeScript، تبدیل شیء و اصلاح ویژگیها را به صورت دینامیک انجام دهید. این کار، قابلیت استفاده مجدد کد و ایمنی نوع را برای توسعهدهندگان جهانی افزایش میدهد.
انواع نگاشته شده (Mapped Types) در TypeScript: تسلط بر تبدیل شیء و اصلاح ویژگی
در چشمانداز همواره در حال تحول توسعه نرمافزار، سیستمهای نوع قوی برای ساخت برنامههای قابل نگهداری، مقیاسپذیر و قابل اعتماد بسیار حیاتی هستند. TypeScript، با قابلیت استنتاج نوع قدرتمند و ویژگیهای پیشرفتهاش، به ابزاری ضروری برای توسعهدهندگان در سراسر جهان تبدیل شده است. یکی از قویترین قابلیتهای آن انواع نگاشته شده (Mapped Types) است، یک مکانیزم پیچیده که به ما امکان میدهد انواع شیء موجود را به انواع جدیدی تبدیل کنیم. این پست وبلاگ به عمق دنیای انواع نگاشته شده TypeScript خواهد پرداخت، مفاهیم اساسی، کاربردهای عملی و چگونگی توانمندسازی توسعهدهندگان برای مدیریت ظریف تبدیلهای شیء و اصلاحات ویژگیها را بررسی میکند.
درک مفهوم اصلی انواع نگاشته شده
در هسته خود، یک نوع نگاشته شده (Mapped Type) روشی برای ایجاد انواع جدید با تکرار بر روی ویژگیهای یک نوع موجود است. آن را به عنوان یک حلقه برای انواع در نظر بگیرید. برای هر ویژگی در نوع اصلی، میتوانید یک تبدیل را روی کلید، مقدار یا هر دو اعمال کنید. این امر طیف وسیعی از امکانات را برای تولید تعاریف نوع جدید بر اساس انواع موجود، بدون تکرار دستی، باز میکند.
ساختار پایه برای یک نوع نگاشته شده شامل { [P in K]: T } است، که در آن:
P: نشاندهنده نام ویژگی است که بر روی آن تکرار میشود.in K: این بخش حیاتی است و نشان میدهد کهPهر کلید را از نوعK(که معمولاً یک اجتماع از رشتههای لفظی، یا یک نوعkeyofاست) خواهد گرفت.T: نوع مقدار را برای ویژگیPدر نوع جدید تعریف میکند.
بیایید با یک مثال ساده شروع کنیم. فرض کنید یک شیء دارید که دادههای کاربر را نشان میدهد و میخواهید یک نوع جدید ایجاد کنید که در آن همه ویژگیها اختیاری باشند. این یک سناریوی رایج است، برای مثال، هنگام ساخت اشیاء پیکربندی یا هنگام پیادهسازی بهروزرسانیهای جزئی.
مثال ۱: اختیاری کردن تمام ویژگیها
این نوع پایه را در نظر بگیرید:
type User = {
id: number;
name: string;
email: string;
isActive: boolean;
};
ما میتوانیم یک نوع جدید، OptionalUser، ایجاد کنیم که در آن همه این ویژگیها با استفاده از یک نوع نگاشته شده (Mapped Type) اختیاری هستند:
type OptionalUser = {
[P in keyof User]?: User[P];
};
بیایید این را جزئیتر بررسی کنیم:
keyof User: این یک اجتماع از کلیدهای نوعUserرا تولید میکند (مانند،'id' | 'name' | 'email' | 'isActive').P in keyof User: این بر روی هر کلید در اجتماع تکرار میشود.?: این اصلاحکنندهای است که ویژگی را اختیاری میکند.User[P]: این یک نوع جستجو (lookup type) است. برای هر کلیدP، نوع مقدار مربوطه را از نوع اصلیUserبازیابی میکند.
نوع OptionalUser حاصل به این شکل خواهد بود:
{
id?: number;
name?: string;
email?: string;
isActive?: boolean;
}
این فوقالعاده قدرتمند است. به جای تعریف مجدد دستی هر ویژگی با ?، ما نوع را به صورت پویا تولید کردهایم. این اصل میتواند برای ایجاد بسیاری از انواع کاربردی دیگر گسترش یابد.
اصلاحکنندههای رایج ویژگی در انواع نگاشته شده
انواع نگاشته شده فقط برای اختیاری کردن ویژگیها نیستند. آنها به شما اجازه میدهند اصلاحکنندههای مختلفی را بر روی ویژگیهای نوع حاصل اعمال کنید. رایجترین آنها عبارتند از:
- اختیاری بودن (Optionality): اضافه کردن یا حذف کردن اصلاحکننده
?. - فقط خواندنی (Readonly): اضافه کردن یا حذف کردن اصلاحکننده
readonly. - قابلیت Null بودن/غیر Null بودن (Nullability/Non-nullability): اضافه کردن یا حذف کردن
| nullیا| undefined.
مثال ۲: ایجاد یک نسخه فقط خواندنی از یک نوع
مشابه اختیاری کردن ویژگیها، میتوانیم یک نوع ReadonlyUser ایجاد کنیم:
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
این نتیجه خواهد داد:
{
readonly id: number;
readonly name: string;
readonly email: string;
readonly isActive: boolean;
}
این برای اطمینان از اینکه ساختارهای داده خاص، پس از ایجاد، قابل تغییر نیستند، بسیار مفید است؛ این یک اصل اساسی برای ساخت سیستمهای قوی و قابل پیشبینی، به ویژه در محیطهای همزمان یا هنگام کار با الگوهای داده تغییرناپذیر که در پارادایمهای برنامهنویسی تابعی که توسط بسیاری از تیمهای توسعه بینالمللی پذیرفته شدهاند، رایج است.
مثال ۳: ترکیب اختیاری بودن و فقط خواندنی بودن
میتوانیم اصلاحکنندهها را ترکیب کنیم. برای مثال، نوعی که ویژگیهایش هم اختیاری و هم فقط خواندنی باشند:
type OptionalReadonlyUser = {
readonly [P in keyof User]?: User[P];
};
این منجر به:
{
readonly id?: number;
readonly name?: string;
readonly email?: string;
readonly isActive?: boolean;
}
حذف اصلاحکنندهها با انواع نگاشته شده
اگر بخواهید یک اصلاحکننده را حذف کنید چه؟ TypeScript این امکان را با استفاده از نحو -? و -readonly در داخل انواع نگاشته شده فراهم میکند. این به ویژه هنگام کار با انواع کاربردی موجود یا ترکیبات نوع پیچیده قدرتمند است.
فرض کنید یک نوع Partial<T> دارید (که داخلی است و تمام ویژگیها را اختیاری میکند)، و میخواهید نوعی را ایجاد کنید که همانند Partial<T> باشد اما با تمام ویژگیهای دوباره اجباری شده.
type Mandatory<T> = {
-?: T extends object ? T[keyof T] : never;
};
type FullyPopulatedUser = Mandatory<Partial<User>>;
این کمی غیرمنتظره به نظر میرسد. بیایید آن را تحلیل کنیم:
Partial<User> معادل OptionalUser ما است. اکنون، میخواهیم ویژگیهای آن را اجباری کنیم. نحو -? اصلاحکننده اختیاری بودن را حذف میکند.
یک راه مستقیمتر برای دستیابی به این هدف، بدون اتکا به Partial ابتدا، این است که به سادگی نوع اصلی را بگیرید و در صورت اختیاری بودن، آن را اجباری کنید:
type MakeMandatory<T> = {
-?: T;
};
type MandatoryUser = MakeMandatory<OptionalUser>;
این به درستی OptionalUser را به ساختار نوع اصلی User (تمام ویژگیها موجود و الزامی) برمیگرداند.
به طور مشابه، برای حذف اصلاحکننده readonly:
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type MutableUser = Mutable<ReadonlyUser>;
MutableUser معادل نوع اصلی User خواهد بود، اما ویژگیهای آن فقط خواندنی نخواهند بود.
قابلیت Null و Undefined بودن
شما همچنین میتوانید قابلیت Null بودن را کنترل کنید. برای مثال، برای اطمینان از اینکه همه ویژگیها قطعاً غیر-null هستند:
type NonNullableValues<T> = {
[P in keyof T]-?: NonNullable<T[P]>;
};
interface MaybeNull {
name: string | null;
age: number | undefined;
}
type DefiniteValues = NonNullableValues<MaybeNull>;
// type DefiniteValues = {
// name: string;
// age: number;
// }
در اینجا، -? تضمین میکند که ویژگیها اختیاری نیستند، و NonNullable<T[P]> مقادیر null و undefined را از نوع مقدار حذف میکند.
تبدیل کلیدهای ویژگی
انواع نگاشته شده به طرز باورنکردنی متنوع هستند و تنها به اصلاح مقادیر یا اصلاحکنندهها محدود نمیشوند. شما همچنین میتوانید کلیدهای یک نوع شیء را تبدیل کنید. اینجاست که انواع نگاشته شده در سناریوهای پیچیده واقعاً میدرخشند.
مثال ۴: پیشوندگذاری کلیدهای ویژگی
فرض کنید میخواهید یک نوع جدید ایجاد کنید که در آن تمام ویژگیهای یک نوع موجود دارای یک پیشوند خاص باشند. این میتواند برای فضای نامگذاری یا برای تولید نسخههای مختلف ساختارهای داده مفید باشد.
type Prefixed<T, Prefix extends string> = {
[P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};
type OriginalConfig = {
timeout: number;
retries: number;
};
type PrefixedConfig = Prefixed<OriginalConfig, 'app'>;
// type PrefixedConfig = {
// appTimeout: number;
// appRetries: number;
// }
بیایید تبدیل کلید را تشریح کنیم:
P in keyof T: همچنان روی کلیدهای اصلی تکرار میشود.as `${Prefix}${Capitalize<string & P>}`: این بند بازنگاشت کلید است.`${Prefix}${...}`: این از انواع لفظی الگو (template literal types) برای ساخت نام کلید جدید با الحاقPrefixارائه شده با نام ویژگی تبدیل شده استفاده میکند.Capitalize<string & P>: این یک الگوی رایج برای اطمینان از اینکه نام ویژگیPبه عنوان یک رشته در نظر گرفته شود و سپس حروف اول آن بزرگ شود. ما ازstring & Pبرای اشتراکگذاریPباstringاستفاده میکنیم، که اطمینان میدهد TypeScript آن را به عنوان یک نوع رشته در نظر میگیرد، که برایCapitalizeضروری است.
این مثال نشان میدهد که چگونه میتوانید ویژگیها را به صورت دینامیک بر اساس ویژگیهای موجود تغییر نام دهید، یک تکنیک قدرتمند برای حفظ سازگاری در لایههای مختلف یک برنامه یا هنگام ادغام با سیستمهای خارجی که دارای قراردادهای نامگذاری خاصی هستند.
مثال ۵: فیلتر کردن ویژگیها
اگر فقط بخواهید ویژگیهایی را شامل شوید که یک شرط خاص را برآورده میکنند چه؟ این را میتوان با ترکیب انواع نگاشته شده با انواع شرطی (Conditional Types) و بند as برای بازنگاشت کلید، که اغلب برای فیلتر کردن ویژگیها استفاده میشود، به دست آورد.
type OnlyStrings<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
interface MixedData {
name: string;
age: number;
city: string;
isActive: boolean;
}
type StringOnlyData = OnlyStrings<MixedData>;
// type StringOnlyData = {
// name: string;
// city: string;
// }
در این حالت:
T[P] extends string ? P : never: برای هر ویژگیP، بررسی میکنیم که آیا نوع مقدار آن (T[P]) قابل انتساب بهstringاست یا خیر.- اگر رشته باشد، کلید
Pحفظ میشود. - اگر رشته نباشد، به
neverنگاشته میشود. هنگامی که یک کلید بهneverنگاشته میشود، عملاً از نوع شیء حاصل حذف میگردد.
این تکنیک برای ایجاد انواع خاصتر از انواع گستردهتر، به عنوان مثال، استخراج فقط تنظیمات پیکربندی که از یک نوع خاص هستند، یا جدا کردن فیلدهای داده بر اساس ماهیت آنها، بسیار ارزشمند است.
مثال ۶: تبدیل کلیدها به شکلی متفاوت
شما همچنین میتوانید کلیدها را به انواع کاملاً متفاوتی از کلیدها تبدیل کنید، برای مثال، تبدیل کلیدهای رشتهای به اعداد، یا برعکس، اگرچه این کار برای دستکاری مستقیم شیء کمتر رایج است و بیشتر برای برنامهنویسی سطح نوع پیشرفته کاربرد دارد.
تبدیل کلیدهای رشتهای به یک اجتماع از رشتههای لفظی، و سپس استفاده از آن به عنوان پایه برای یک نوع جدید را در نظر بگیرید. اگرچه این روش مستقیماً کلیدهای یک شیء را درون خود نوع نگاشته شده به این شیوه خاص تبدیل نمیکند، اما نشان میدهد که چگونه میتوان کلیدها را دستکاری کرد.
یک مثال مستقیمتر از تبدیل کلید میتواند نگاشت کلیدها به نسخههای حروف بزرگ آنها باشد:
type UppercaseKeys<T> = {
[P in keyof T as Uppercase<string & P>]: T[P];
};
type LowercaseData = {
firstName: string;
lastName: string;
};
type UppercaseData = UppercaseKeys<LowercaseData>;
// type UppercaseData = {
// FIRSTNAME: string;
// LASTNAME: string;
// }
این از بند as برای تبدیل هر کلید P به معادل حروف بزرگ آن استفاده میکند.
کاربردهای عملی و سناریوهای دنیای واقعی
انواع نگاشته شده تنها ساختارهای نظری نیستند؛ آنها کاربردهای عملی قابل توجهی در حوزههای مختلف توسعه دارند. در اینجا چند سناریوی رایج که در آنها این انواع بسیار ارزشمند هستند، آورده شدهاند:
۱. ساخت انواع کاربردی قابل استفاده مجدد
بسیاری از تبدیلهای نوع رایج را میتوان در انواع کاربردی قابل استفاده مجدد کپسوله کرد. کتابخانه استاندارد TypeScript قبلاً نمونههای عالی مانند Partial<T>، Readonly<T>، Record<K, T> و Pick<T, K> را ارائه میدهد. شما میتوانید انواع کاربردی سفارشی خود را با استفاده از انواع نگاشته شده برای سادهسازی گردش کار توسعه خود تعریف کنید.
برای مثال، نوعی که تمام ویژگیها را به توابعی نگاشت میکند که مقدار اصلی را پذیرفته و یک مقدار جدید بازمیگردانند:
type Mappers<T> = {
[P in keyof T]: (value: T[P]) => T[P];
};
interface ProductInfo {
name: string;
price: number;
}
type ProductMappers = Mappers<ProductInfo>;
// type ProductMappers = {
// name: (value: string) => string;
// price: (value: number) => number;
// }
۲. مدیریت و اعتبارسنجی فرم دینامیک
در توسعه فرانتاند، به ویژه با فریمورکهایی مانند React یا Angular (هرچند مثالهای اینجا فقط TypeScript هستند)، مدیریت فرمها و وضعیتهای اعتبارسنجی آنها یک وظیفه رایج است. انواع نگاشته شده میتوانند به مدیریت وضعیت اعتبارسنجی هر فیلد فرم کمک کنند.
فرمی را در نظر بگیرید که فیلدهای آن میتوانند 'pristine' (دستنخورده)، 'touched' (لمسشده)، 'dirty' (تغییریافته)، 'valid' (معتبر) یا 'invalid' (نامعتبر) باشند.
type FormFieldState = 'pristine' | 'touched' | 'dirty' | 'valid' | 'invalid';
type FormState<T> = {
[P in keyof T]: FormFieldState;
};
interface UserForm {
username: string;
email: string;
password: string;
}
type UserFormState = FormState<UserForm>;
// type UserFormState = {
// username: FormFieldState;
// email: FormFieldState;
// password: FormFieldState;
// }
این به شما امکان میدهد نوعی ایجاد کنید که ساختار داده فرم شما را منعکس میکند اما به جای آن وضعیت هر فیلد را ردیابی میکند، که انسجام و ایمنی نوع را برای منطق مدیریت فرم شما تضمین میکند. این امر به ویژه برای پروژههای بینالمللی که الزامات UI/UX متنوع ممکن است منجر به وضعیتهای فرم پیچیدهای شود، مفید است.
۳. تبدیل پاسخ API
هنگام کار با APIها، دادههای پاسخ ممکن است همیشه کاملاً با مدلهای دامنه داخلی شما مطابقت نداشته باشند. انواع نگاشته شده میتوانند در تبدیل پاسخهای API به شکل مورد نظر کمک کنند.
یک پاسخ API را تصور کنید که از snake_case برای کلیدها استفاده میکند، اما برنامه شما camelCase را ترجیح میدهد:
// Assume this is the incoming API response type
type ApiUserData = {
user_id: number;
first_name: string;
last_name: string;
};
// Helper to convert snake_case to camelCase for keys
type ToCamelCase<S extends string>: string = S extends `${infer T}_${infer U}`
? `${T}${Capitalize<U>}`
: S;
type CamelCasedKeys<T> = {
[P in keyof T as ToCamelCase<string & P>]: T[P];
};
type AppUserData = CamelCasedKeys<ApiUserData>;
// type AppUserData = {
// userId: number;
// firstName: string;
// lastName: string;
// }
این یک مثال پیشرفتهتر است که از یک نوع شرطی بازگشتی برای دستکاری رشته استفاده میکند. نکته کلیدی این است که انواع نگاشته شده، هنگامی که با سایر ویژگیهای پیشرفته TypeScript ترکیب میشوند، میتوانند تبدیلهای پیچیده داده را خودکار کنند، زمان توسعه را کاهش داده و خطر خطاهای زمان اجرا را کم کنند. این برای تیمهای جهانی که با خدمات بکاند متنوع کار میکنند حیاتی است.
۴. بهبود ساختارهای شبیه Enum
در حالی که TypeScript دارای enum است، گاهی اوقات ممکن است بخواهید انعطافپذیری بیشتری داشته باشید یا انواع را از لفظهای شیء که مانند enum عمل میکنند، استخراج کنید.
const AppPermissions = {
READ: 'read',
WRITE: 'write',
DELETE: 'delete',
ADMIN: 'admin',
} as const;
type Permission = typeof AppPermissions[keyof typeof AppPermissions];
// type Permission = 'read' | 'write' | 'delete' | 'admin'
type UserPermissions = {
[P in Permission]?: boolean;
};
type RolePermissions = {
[P in Permission]: boolean;
};
const userPerms: UserPermissions = {
read: true,
};
const adminRole: RolePermissions = {
read: true,
write: true,
delete: true,
admin: true,
};
در اینجا، ابتدا یک نوع اجتماع (union type) از تمام رشتههای مجوز ممکن را استخراج میکنیم. سپس، از انواع نگاشته شده برای ایجاد انواعی استفاده میکنیم که در آنها هر مجوز یک کلید است، به ما این امکان را میدهد که مشخص کنیم آیا یک کاربر آن مجوز را دارد (اختیاری) یا یک نقش آن را الزامی میکند (اجباری). این الگو در سیستمهای اعتبارسنجی در سراسر جهان رایج است.
چالشها و ملاحظات
در حالی که انواع نگاشته شده به طرز باورنکردنی قدرتمند هستند، مهم است که از پیچیدگیهای بالقوه آگاه باشید:
- خوانایی و پیچیدگی: انواع نگاشته شده بیش از حد پیچیده میتوانند دشوار برای خواندن و درک شوند، به خصوص برای توسعهدهندگانی که با این ویژگیهای پیشرفته تازهکار هستند. همیشه برای وضوح تلاش کنید و اضافه کردن نظرات یا تقسیم کردن تبدیلهای پیچیده را در نظر بگیرید.
- پیامدهای عملکرد: در حالی که بررسی نوع TypeScript در زمان کامپایل انجام میشود، دستکاریهای نوع بسیار پیچیده میتوانند، در تئوری، زمان کامپایل را کمی افزایش دهند. برای اکثر برنامهها، این قابل اغماض است، اما برای پایگاههای کد بسیار بزرگ یا فرآیندهای ساخت بسیار حیاتی از نظر عملکرد، نکتهای است که باید به خاطر سپرده شود.
- اشکالزدایی: هنگامی که یک نوع نگاشته شده نتیجه غیرمنتظرهای تولید میکند، اشکالزدایی گاهی اوقات میتواند چالشبرانگیز باشد. استفاده از TypeScript Playground یا ویژگیهای بازرسی نوع IDE برای درک نحوه حل شدن انواع حیاتی است.
- درک
keyofو انواع Lookup: استفاده مؤثر از انواع نگاشته شده بر درک قوی ازkeyofو انواع lookup (T[P]) متکی است. اطمینان حاصل کنید که تیم شما درک خوبی از این مفاهیم بنیادی دارد.
بهترین روشها برای استفاده از انواع نگاشته شده
برای بهرهبرداری از پتانسیل کامل انواع نگاشته شده در عین کاهش چالشهای آنها، این بهترین روشها را در نظر بگیرید:
- ساده شروع کنید: قبل از ورود به بازنگاشتهای پیچیده کلید یا منطق شرطی، با تبدیلهای اختیاری و فقط خواندنی پایه شروع کنید.
- از انواع کاربردی داخلی استفاده کنید: با انواع کاربردی داخلی TypeScript مانند
Partial،Readonly،Record،Pick،OmitوExcludeآشنا شوید. آنها اغلب برای وظایف رایج کافی هستند و به خوبی آزمایش شده و درک شدهاند. - انواع عمومی قابل استفاده مجدد ایجاد کنید: الگوهای رایج انواع نگاشته شده را در انواع کاربردی عمومی کپسوله کنید. این امر به ارتقای سازگاری و کاهش کدهای تکراری در پروژه شما و برای تیمهای جهانی کمک میکند.
- از نامهای توصیفی استفاده کنید: انواع نگاشته شده و پارامترهای عمومی خود را به وضوح نامگذاری کنید تا هدف آنها را نشان دهد (مثلاً
Optional<T>،DeepReadonly<T>،PrefixedKeys<T, Prefix>). - خوانایی را اولویت قرار دهید: اگر یک نوع نگاشته شده بیش از حد پیچیده شد، بررسی کنید که آیا راه سادهتری برای دستیابی به همان نتیجه وجود دارد یا خیر و آیا پیچیدگی اضافه شده ارزشش را دارد. گاهی اوقات، یک تعریف نوع کمی پرجزئیاتتر اما واضحتر ارجح است.
- انواع پیچیده را مستند کنید: برای انواع نگاشته شده پیچیده، نظرات JSDoc را برای توضیح عملکرد آنها اضافه کنید، به ویژه هنگام به اشتراک گذاشتن کد در یک تیم بینالمللی متنوع.
- انواع خود را آزمایش کنید: تستهای نوع بنویسید یا از مثالها برای تأیید اینکه انواع نگاشته شده شما طبق انتظار عمل میکنند، استفاده کنید. این امر به ویژه برای تبدیلهای پیچیده که باگهای ظریف میتوانند دشوار باشد، مهم است.
نتیجهگیری
انواع نگاشته شده TypeScript سنگ بنای دستکاری پیشرفته نوع هستند و قدرت بینظیری را برای تبدیل و انطباق انواع شیء به توسعهدهندگان ارائه میدهند. چه در حال اختیاری کردن ویژگیها، فقط خواندنی کردن، تغییر نام دادن آنها یا فیلتر کردن آنها بر اساس شرایط پیچیده باشید، انواع نگاشته شده راهی اعلامی، ایمن از نظر نوع، و بسیار گویا برای مدیریت ساختارهای داده شما فراهم میکنند.
با تسلط بر این تکنیکها، میتوانید قابلیت استفاده مجدد کد را به طور قابل توجهی افزایش دهید، ایمنی نوع را بهبود بخشید و برنامههای قویتر و قابل نگهداریتری بسازید. قدرت انواع نگاشته شده را در آغوش بگیرید تا توسعه TypeScript خود را ارتقا دهید و به ساخت راهحلهای نرمافزاری با کیفیت بالا برای مخاطبان جهانی کمک کنید. همانطور که با توسعهدهندگان از مناطق مختلف همکاری میکنید، این الگوهای نوع پیشرفته میتوانند به عنوان یک زبان مشترک برای تضمین کیفیت و سازگاری کد، و پر کردن شکافهای ارتباطی بالقوه از طریق دقت سیستم نوع، عمل کنند.