Record<K, V> نوع أداة مدمج لكائن مفاتيحه من النوع K وكل قيمه من النوع V. مع مفاتيح string يصف قاموسًا؛ ومع union من المفاتيح الحرفية يصف كائنًا يجب أن يملك هذه المفاتيح تحديدًا.
Record موجود في نظام الأنواع فقط. وقت التشغيل الكائنان كائنا JavaScript عاديان، فيعملان مع الكائنات الحرفية والنشر وJSON.stringify وكل ما يأخذ كائنًا.
الصياغة والتعريف
Record<Keys, Value>
يجب أن يكون Keys شيئًا يصلح مفتاحًا لكائن: string أو number أو symbol أو union من أنواع حرفية نصية أو رقمية أو نوع template literal. ويمكن أن يكون Value أي نوع. التعريف كاملًا في المكتبة القياسية لـ TypeScript سطر واحد، وهو mapped type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any هو string | number | symbol، أي مجموعة كل أنواع المفاتيح الممكنة. و[P in K]: T ينشئ خاصية واحدة من النوع T لكل عضو في K. وهذا يفسر السلوكين التاليين: K الواسع مثل string ينتج index signature (أي مفتاح)، وK من نوع union ينتج خاصية إلزامية لكل عضو.
مفاتيح union: كل مفتاح إلزامي
عندما تكون المفاتيح union من أنواع حرفية، يجب أن يذكر Record كلًّا منها، ولا شيء غيرها. وهذا يحوّل المترجم إلى قائمة تحقق:
أضف "cancelled" إلى Status فيتوقف الكائنان عن الترجمة حتى تعطي الحالة الجديدة تسمية ولونًا. وحذف مفتاح، أو إضافة مفتاح ليس في union، خطأ ترجمة:
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
ويعمل الأمر نفسه مع enum نصي كنوع للمفاتيح: Record<Color, string> يشترط مُدخلًا لكل عضو في الـ enum.
Record<string, T> والمفتاح المفقود
مع مفاتيح string يُسمح بأي مفتاح، وتعطي TypeScript كل عملية قراءة النوع T، حتى لمفتاح غير موجود. ووقت التشغيل يعطي المفتاح المفقود undefined:
هذا أشيع أخطاء Record. وهناك ثلاث طرق للتعامل معه: افحص بـ in أو Object.hasOwn قبل القراءة، أو صرّح بالقيمة كـ V | undefined، أو فعّل خيار المترجم noUncheckedIndexedAccess الذي يضيف | undefined إلى كل قراءة عبر index signature في المشروع. أما Record بمفاتيح union فلا يعاني هذه المشكلة، لأن وجود كل مفتاح مضمون.
Partial<Record<K, V>>: بعض المفاتيح فقط
لتستخدم union من المفاتيح دون أن تشترطها كلها، غلّف الـ Record بـ Partial. عندها تعيد القراءات V | undefined، وهذا هو الوصف الصادق:
المفتاح المكتوب خطأً مثل jp يبقى خطأ ترجمة، وهذه هي الميزة مقارنة بـ Record<string, string>.
المرور على Record
Object.keys وObject.values وObject.entries تعمل كلها. والمشكلة في نوع المفتاح: Object.keys يعيد string[] وObject.entries يعيد [string, V][]، ولا يعيدان أبدًا union المفاتيح الخاص بك:
تُبقي TypeScript المفاتيح بالنوع string عن قصد: قد يملك الكائن وقت التشغيل خصائص أكثر مما يذكره نوعه، فالوعد بـ Plan[] غير آمن عمومًا. أما لكائن أنشأته من كائن حرفي، مثل seats، فالتحويل آمن.
بناء Record من البيانات
الـ Records هي النتيجة المعتادة لتجميع مصفوفة أو فهرستها. ابدأ من كائن فارغ بنوع Record ثم املأه:
يعيد Book["genre"] استخدام union من الـ interface كنوع للمفاتيح، فإضافة نوع كتب جديد إلى Book تجعل byGenre يطلب مُدخلًا جديدًا.
Record<string, unknown> والـ interfaces
Record<string, unknown> نوع شائع لـ «كائن ما بمفاتيح نصية». يقبل الكائنات الحرفية والقيم المعرّفة بـ type alias، لكنه يرفض الـ interface:
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
يمكن توسيع الـ interfaces عبر دمج التعريفات (declaration merging)، لذا لا تفترض TypeScript أنها تناسب index signature؛ أما type aliases فلا يمكن إعادة فتحها، فالنوع type User = { name: string } كان سيمر. والحلول المعتادة: اقبل object بدلًا منه (ويمكنك مع ذلك استدعاء Object.keys عليه)، أو اجعل الدالة generic (<T extends object>(obj: T))، أو عرّف User بـ type (تشرح صفحة interface أم type بقية الفروق).
Record أم index signature أم Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| موجود وقت التشغيل | لا، كائن عادي | لا، كائن عادي | نعم، صنف |
| مجموعة مفاتيح ثابتة | نعم، مع K من نوع union | لا | لا |
| أنواع المفاتيح | string وnumber وsymbol وunions حرفية وأنماط template | string وnumber وsymbol وأنماط template | أي شيء، ومنه الكائنات |
| نوع قراءة مفتاح مفقود | V (مع مفاتيح string) | V | V | undefined من get |
| المزج مع خصائص مسماة | عبر intersection & | نعم، في النوع نفسه | لا |
| JSON والنشر | نعم | نعم | لا، حوّله أولًا |
| الحجم | Object.keys(r).length | Object.keys(o).length | m.size |
| الإضافة والحذف المتكرران | يعمل | يعمل | مصمم لذلك |
اختر Record بمفاتيح union كلما كانت مجموعة المفاتيح معروفة: إنه الخيار الوحيد الذي يتحقق من وجود كل مفتاح. وللمفاتيح النصية المفتوحة، Record<string, V> والـ index signature قابلان للتبادل، وكثير من المشاريع تفضّل Record لسهولة قراءته. والجأ إلى Map عندما تُضاف المفاتيح وتُحذف وقت التشغيل، أو عندما لا تكون المفاتيح نصوصًا، أو عندما تحتاج إلى الحجم وترتيب الإدراج دون عمل إضافي.
الأسئلة الشائعة
ما هو Record في TypeScript؟
Record<K, V> نوع أداة مدمج لكائن مفاتيحه من النوع K وكل قيمه من النوع V. Record<string, number> كائن بأي مفاتيح نصية وقيم رقمية؛ وRecord<"en" | "de", string> كائن بالمفتاحين en وde تحديدًا، وكلاهما نص.
ما الفرق بين Record وMap في TypeScript؟
Record نوع لكائن JavaScript عادي، فيعمل مع الكائنات الحرفية وJSON والنشر (spread)، ويختفي وقت الترجمة. أما Map فصنف موجود وقت التشغيل فيه get وset وhas وsize، ويحفظ ترتيب الإدراج لكل المفاتيح، ويقبل أي نوع مفتاح (والكائنات أيضًا)، وget يعيد V | undefined. استخدم Record للبيانات الثابتة أو ذات شكل JSON، وMap للمفاتيح التي تُضاف وتُحذف وقت التشغيل.
ما الفرق بين Record<string, T> و{ [key: string]: T }؟
من حيث القيم هما النوع نفسه: Record<string, T> يتوسع إلى نوع كائن فيه index signature نصي. وهناك فرقان صغيران: الـ index signature يمكن أن يحمل اسمًا وأن يجاور خصائص أخرى في النوع نفسه، وkeyof Record<string, T> هو string بينما keyof { [key: string]: T } هو string | number.
كيف أمر على Record في TypeScript؟
استخدم Object.entries(record) لأزواج المفتاح والقيمة، وObject.keys للمفاتيح، وObject.values للقيم. تعود المفاتيح بالنوع string لا K، لأن الكائن قد يحمل مفاتيح إضافية وقت التشغيل. وعندما يكون للـ Record مفاتيح union معروفة، حوّل النوع: (Object.keys(r) as Array<keyof typeof r>).
كيف أجعل بعض مفاتيح Record فقط إلزامية؟
مع مفاتيح union يشترط Record<K, V> وجود كل مفتاح. غلّفه بـ Partial لتجعلها كلها اختيارية: Partial<Record<Lang, string>>. وللمزج استخدم intersection: Record<"en", string> & Partial<Record<"de" | "fr", string>> يشترط en ويسمح بالبقية.