Menu
flag Ar iconالعربيةdown icon

الأنواع المعيّنة (Mapped Types) في TypeScript: الصيغة والمعدّلات

النوع المعيّن (mapped type) يبني نوع كائن جديدًا بالمرور على المفاتيح: { [K in keyof T]: ... }. تعرّف على الصيغة، والمعدّلين readonly و? مع + و-، وإعادة تسمية المفاتيح بـ as، وتصفية المفاتيح، وكيف تُكتب Partial وReadonly وRequired وPick وRecord.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

النوع المعيّن يبني نوع كائن جديدًا بالمرور على مجموعة من المفاتيح. يعني { [K in keyof T]: boolean } «لكل مفتاح K في T، خاصية اسمها K من النوع boolean»:

أضف مفتاحًا إلى Features فيطلبه Flags<Features> أيضًا. الأنواع المعيّنة تعمل وقت الترجمة فقط: تصف الكائنات ولا تنشئها.

الصيغة

للنوع المعيّن ثلاثة أجزاء: متغير مفتاح، وunion من المفاتيح للمرور عليها، ونوع الخاصية الذي قد يستخدم متغير المفتاح.

type MappedType = {
  [K in Keys]: PropertyType; // K takes each member of Keys in turn
};

يمكن أن يكون Keys أي union من النصوص أو الأرقام أو الرموز (symbols). ولا يلزم أن يأتي من keyof:

هذا بالضبط ما يفعله Record<Size, number>؛ فـ Record نوع معيّن من سطر واحد. ويمكن لنوع الخاصية أن يستخدم المفتاح: ينسخ { [K in keyof T]: T[K] } نوع كل خاصية عبر وصول مفهرس، ويجعل { [K in keyof T]: T[K] | null } كل خاصية قابلة لأن تكون null.

المعدّلات: readonly و? مع + و-

يمكن للنوع المعيّن إضافة المعدّلين readonly والاختياري (?) إلى كل الخصائص دفعة واحدة أو حذفهما. ضع + للإضافة (وهو الافتراضي حين لا تكتب شيئًا) و- للحذف:

يبيّن السطر الأخير أن readonly مجرد قاعدة وقت الترجمة: بُلّغ عن الإسناد (وأُسكت هنا بـ @ts-expect-error)، لكن JavaScript الناتج نفّذه رغم ذلك. ولا يكتفي -? بحذف علامة الاستفهام: بل يحذف أيضًا undefined من نوع الخاصية، فيرفض AllRequired<Account> القيمة { id: 1, email: undefined } بالخطأ error TS2322: Type 'undefined' is not assignable to type 'string'.

كيف تُبنى Partial وReadonly وPick وRecord

أنواع الأدوات المدمجة التي تعيد تشكيل الكائنات أنواع معيّنة. هذه هي التعريفات من الملف lib.es5.d.ts الخاص بـ TypeScript نفسها:

type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };

قراءتها تمرين جيد: يمر Pick على المفاتيح التي تمررها فقط، ويتجاهل Record أي نوع مصدر ويعطي كل مفتاح نوع القيمة نفسه. أما Omit فليس نوعًا معيّنًا مستقلًا: إنه Pick<T, Exclude<keyof T, K>>. ويسردها كلها مرجع أنواع الأدوات.

إعادة تسمية المفاتيح بـ as

عبارة as بعد المفتاح تغيّر اسم الخاصية. ومع أنواع القوالب النصية (template literal types) يمكنها توليد أسماء جديدة من الأسماء القديمة:

وُضع string & K لأن keyof T قد يتضمن مفاتيح number وsymbol، وCapitalize لا يقبل إلا النصوص. يحتفظ التقاطع (intersection) بالمفاتيح النصية ويحذف الباقي.

تصفية المفاتيح بـ never

إذا أنتجت عبارة as النوع never لمفتاح ما، يُحذف ذلك المفتاح. ومع نوع شرطي يمكنك الإبقاء على الخصائص أو حذفها بحسب نوعها:

أنواع مثل DataOnly مفيدة لوصف ما يبقى بعد JSON.stringify أو ما يعدّله نموذج إدخال: الحقول دون التوابع.

أي المعدّلات تُحفظ

النوع المعيّن على keyof T لنوع ما T يُسمى متماثل الشكل (homomorphic)، وهو ينسخ معدّلي readonly و? لكل خاصية من T. أما النوع المعيّن على union عادي من المفاتيح فيبدأ دون أي معدّلات:

type Account = { readonly id: number; email?: string };

type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string }  (modifiers kept)

type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined }  (email is required now)

لا يزال B يحمل undefined في نوع email، لأن Account["email"] يتضمنه، لكن الخاصية نفسها لم تعد اختيارية: يُرفض { id: 1 }. ولهذا تمر Partial<T> وأخواتها على keyof T: فهي تحتفظ بكل ما لا تغيّره صراحة.

النوع المعيّن المتماثل الشكل العام حين يُطبَّق على مصفوفة أو tuple ينتج مصفوفة أو tuple، لا كائنًا بمفاتيح رقمية. Readonly<string[]> هو readonly string[]، ومع type Box<T> = { [K in keyof T]: { value: T[K] } } يكون Box<[string, number]> هو [{ value: string }, { value: number }].

الأنواع المعيّنة وكود وقت التشغيل

لأن النوع المعيّن يصف الكائن فقط، تحتاج الدالة التي تبني هذا الكائن إلى حلقة خاصة بها وقت التشغيل. ثم يصف النوع نتيجتها:

تحصل الـ callbacks على أنواع معاملاتها من النوع المعيّن (v من النوع string في إحداها وnumber في الأخرى) دون أي توصيف. وإن جئت إلى هنا تبحث عن الصنف Map لا عن تحويل الأنواع، فراجع Map في TypeScript.

الأسئلة الشائعة

ما هو النوع المعيّن (mapped type) في TypeScript؟

نوع ينشئ نوع كائن بالمرور على union من المفاتيح: { [K in Keys]: SomeType }. في أغلب الأحيان تكون المفاتيح keyof T، فيحمل النوع الجديد مفاتيح T نفسها بأنواع خصائص محوّلة، كما في type Flags<T> = { [K in keyof T]: boolean }.

ماذا تعني +readonly و-readonly و+? و-? في النوع المعيّن؟

تضيف المعدّلات أو تحذفها. readonly أو +readonly تجعل كل خاصية للقراءة فقط، و-readonly تحذف readonly. و? أو +? تجعل كل خاصية اختيارية، و-? تجعلها مطلوبة وتحذف أيضًا undefined من أنواعها. ويُكتب Required<T> باستخدام -?.

كيف أعيد تسمية المفاتيح في نوع معيّن؟

استخدم عبارة as بعد المفتاح: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } يحوّل name إلى getName. وتعيين مفتاح إلى never في عبارة as يحذفه، وهكذا تُصفّى الخصائص.

هل النوع المعيّن هو نفسه Map في TypeScript؟

لا. النوع المعيّن تحويل للأنواع وقت الترجمة ولا ينتج أي كود. أما Map<K, V> فهو صنف Map في JavaScript، مجموعة أزواج مفتاح وقيمة وقت التشغيل. وعمليات البحث عن "typescript map type" تقصد أحدهما أو الآخر.

كيف تُنفَّذ Partial وReadonly؟

كأنواع معيّنة في المكتبة القياسية: type Partial<T> = { [P in keyof T]?: T[P] } وtype Readonly<T> = { readonly [P in keyof T]: T[P] }. وPick وRecord أنواع معيّنة أيضًا؛ أما Omit فهو Pick مع Exclude.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن