يأخذ keyof نوع كائن وينتج union من أسماء خصائصه. في المثال التالي keyof User هو النوع "id" | "name" | "email"، فلا يستطيع متغير من هذا النوع أن يحمل إلا أحد هذه النصوص الثلاثة.
يوجد keyof في نظام الأنواع فقط. يُحذف عند ترجمة الكود، فلا يفحص شيئًا وقت التشغيل أبدًا. ما يعطيك إياه ضمان وقت الترجمة بأن المفتاح يسمّي خاصية حقيقية: أعد تسمية email في الـ interface فيصبح كل "email" نوعه keyof User خطأً.
keyof typeof: مفاتيح كائن عادي
يعمل keyof على الأنواع، لا على القيم. لكائن كتبته كقيمة، احصل على نوعه أولًا بـ typeof، ثم خذ المفاتيح:
اقرأه من الداخل إلى الخارج: typeof colors هو { red: string; green: string; blue: string }، وkeyof لهذا النوع هو union المفاتيح. كتابة keyof colors دون typeof تفشل بالخطأ error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'?.
هذه هي الطريقة المعتادة لاشتقاق union من جدول بحث: أضف لونًا إلى الكائن فيكبر النوع Color معه. ولـ typeof معنى ثانٍ وقت التشغيل أيضًا؛ وتشرح صفحة typeof المعنيين.
الوصول إلى الخصائص بأنواع دقيقة عبر الـ Generics
أشيع استخدام لـ keyof هو قيد الـ generic. يقول K extends keyof T إن «K أحد مفاتيح T»، وT[K] هو نوع الخاصية عند ذلك المفتاح:
معامل النوع الثاني مهم. مع key: keyof T دون K يكون نوع القيمة المُعادة T[keyof T]، أي union أنواع كل الخصائص (string | number | boolean هنا)، فيفشل title.toUpperCase() بالخطأ error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean'. أما التقاط المفتاح الدقيق في K فيحافظ على نوع الخاصية الدقيق. وT[K] نوع وصول مفهرس (indexed access type)، تشرحه صفحة أنواع الوصول المفهرس.
الفهرسة بنص عادي
يظهر خطأ شائع جدًا حين تفهرس كائنًا بقيمة string جاءت من مكان آخر:
يطبع المترجم:
index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
No index signature with a parameter of type 'string' was found on type 'Settings'.
قد يصل أي نص إلى read، بما في ذلك "colour"، وSettings لا يقول شيئًا عن هذا المفتاح. هناك حلان: اقبل المفاتيح الحقيقية فقط، أو افحص النص غير المعروف قبل استخدامه.
name is keyof Settings هي type predicate: حين تعيد الدالة true تعامل TypeScript القيمة input على أنها مفتاح داخل if. تشرح صفحة type guards الـ predicates بالتفصيل.
keyof مع توقيعات الفهرسة: string | number
للنوع الذي له توقيع فهرسة، يعيد keyof نوع الفهرس، وتوقيع الفهرسة النصي يعطي string | number وليس string فقط:
تحوّل JavaScript كل مفتاح خاصية رقمي إلى نص، فيكون scores[42] وscores["42"] الخاصية نفسها. وتعبّر TypeScript عن ذلك بإدخال number. ويبيّن الناتج أيضًا قاعدة في JavaScript: المفاتيح الشبيهة بالأعداد الصحيحة تُسرد أولًا، بترتيب تصاعدي، قبل المفاتيح النصية الأخرى. حين تحتاج دالة عامة إلى المفاتيح النصية فقط، اكتب Extract<keyof T, string> أو string & keyof T؛ ولـ Exclude وExtract صفحة خاصة.
نتائج keyof في لمحة
| النوع | ما يعطيه keyof |
|---|---|
{ id: number; name: string } | "id" | "name" |
{ [key: string]: number } | string | number |
{ [index: number]: string } | number |
{} | never |
any | string | number | symbol |
{ a: 1; b: 2 } | { a: 3; c: 4 } | "a" (المفاتيح الموجودة في كل عضو) |
{ a: 1 } & { c: 4 } | "a" | "c" (مفاتيح أي منهما) |
string[] | number إضافة إلى اسم كل تابع للمصفوفات ("length"، "push"، ...) |
يبدو صفا الـ union والـ intersection معكوسين في البداية. القيمة من نوع union قد تكون أيًا من العضوين، فالآمن فقط المفاتيح الموجودة في كليهما. أما القيمة من نوع intersection فتحمل كل ما في النوعين، فلها كل المفاتيح.
لماذا يعيد Object.keys النوع string[]
نوع Object.keys(obj) هو string[]، وليس (keyof T)[] أبدًا. والسبب هو الأنواع البنيوية (structural typing): قد تحمل القيمة خصائص أكثر من نوعها المُعلن.
نوع p هو Point، ومع ذلك له المفتاح z وقت التشغيل. لو وعد Object.keys بالنوع ("x" | "y")[] لكان الكود المعتمد على ذلك خاطئًا مع p. التأكيد آمن مع كائنات الـ literal التي أنشأتها بنفسك؛ أما الكائنات القادمة من الخارج فأبقِ مفاتيحها string وافحصها.
keyof في الأنواع المعيّنة (Mapped Types)
keyof هو أيضًا طريقة المرور على مفاتيح نوع لبناء نوع جديد. هكذا كُتبت أنواع الأدوات المدمجة Partial وReadonly:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
interface User {
id: number;
name: string;
}
type UserDraft = MyPartial<User>; // { id?: number; name?: string }
يزور [K in keyof T] كل مفتاح، ويقرأ T[K] نوع الخاصية عند ذلك المفتاح. وللأنواع المعيّنة صفحة خاصة تشرح المعدّلات وإعادة تسمية المفاتيح.
الأسئلة الشائعة
ماذا يفعل keyof في TypeScript؟
ينتج keyof T نوع union من أسماء خصائص النوع T. للنوع interface User { id: number; name: string } يكون keyof User هو "id" | "name". يعمل على الأنواع فقط، وقت الترجمة، ولا ينتج أي JavaScript.
ما هو keyof typeof في TypeScript؟
يحتاج keyof إلى نوع، لكن الكائن العادي مثل const colors = { red: "#f00" } قيمة. يحوّل typeof colors القيمة إلى نوعها، ثم يعطي keyof typeof colors union مفاتيحه، وهو هنا "red". أما كتابة keyof colors فتفشل بالخطأ TS2749.
لماذا يعيد keyof النوع string | number؟
حين يكون للنوع توقيع فهرسة نصي مثل { [key: string]: number }. تحوّل JavaScript مفاتيح الخصائص الرقمية إلى نصوص، فيكون obj[42] وصولًا صالحًا أيضًا، ولذلك تُدخل TypeScript النوع number في نوع المفتاح. استخدم Extract<keyof T, string> أو string & keyof T إن كنت تريد المفاتيح النصية فقط.
لماذا يعيد Object.keys النوع string[] بدل (keyof T)[]؟
لأن الكائن قد يحمل خصائص أكثر مما يسرده نوعه: يمكن إسناد قيمة فيها حقل إضافي إلى نوع أضيق. تحديد النتيجة بالنوع (keyof T)[] سيكون كذبًا مع هذه الكائنات. إن كنت تعرف أن الكائن يحمل المفاتيح المعلنة بالضبط، فاستخدم التأكيد: Object.keys(obj) as (keyof typeof obj)[].
كيف أصلح الخطأ "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type"؟
الفهرس string عادي، ونوع الكائن ليس له توقيع فهرسة نصي. حدد نوع المعامل بـ keyof YourType بدل string، أو افحص النص أولًا بـ type guard يضيّقه إلى keyof YourType، أو أعطِ الكائن توقيع فهرسة أو النوع Record<string, V> إن كان يقبل فعلًا أي مفتاح.