اكتشف كيف تحول ٢٠ سطراً من الكود المكرر إلى سطر واحد باستخدام Utility Types في TypeScript، مع أمثلة حقيقية من مشاريع الإنتاج وتجنب الفخاخ الشائعة التي يقع فيها حتى المحترفون.
تخيل أنك تعمل على مشروع ضخم فيه ٣٠ واجهة TypeScript، وكل واجهة تحتاج نسخة معدلة قليلاً: بعضها بدون خاصية معينة، وبعضها بخواص اختيارية، وبعضها بقراءة فقط. بدلاً من كتابة ٣٠ نسخة يدوياً، تخيل أن هناك ساحراً يقول لك: "أعطني النوع الأساسي وسأصنع لك كل النسخ المطلوبة في سطر واحد". هذا الساحر موجود بالفعل، اسمه Utility Types في TypeScript، وهو أحد أقوى الأدوات التي قللت الكود المكرر في مشاريعي بنسبة ٥٠٪ على الأقل.
في هذا الدليل العملي، لن نكتفي بشرح ما هي Utility Types، بل سنغوص في كيفية عملها خلف الكواليس، متى تستخدمها ومتى تتجنبها، والأخطاء الشائعة التي يقع فيها حتى المطورون ذوو الخمس سنوات خبرة. سأريك أمثلة حقيقية من مشاريع إنتاجية، مثل كيفية تحويل واجهة API ضخمة إلى نسخة مختصرة للـ Frontend بدون فقدان الأمان النوعي، وكيفية التعامل مع البيانات القادمة من قواعد البيانات التي قد تكون null أو undefined دون أن تتحول شاشتك إلى بحر من الأخطاء الحمراء.
لنبدأ بأبسط الأدوات وأكثرها استخداماً: `Partial<T>` و`Required<T>`. تخيل أنك تعمل على نموذج بيانات لمستخدم، ولديك واجهة `User` تحتوي على ١٠ خواص، منها ٣ إلزامية والباقي اختياري. عندما تريد تحديث بيانات المستخدم، فأنت تحتاج كل الخواص أن تكون اختيارية، لأنك قد ترسل فقط خاصية واحدة للتحديث. بدلاً من كتابة واجهة جديدة يدوياً، `Partial<User>` تفعل ذلك تلقائياً.
لكن كيف يعمل هذا خلف الكواليس؟ TypeScript لا يولد كوداً جديداً في وقت التشغيل، بل هو مجرد أداة تحليل ثابت. عندما تستخدم `Partial<User>`، فإن TypeScript يقوم بتحويل كل خاصية في `User` إلى optional باستخدام `| undefined`. هذا يعني أن الكود التالي يصبح صالحاً:
interface User {
id: string;
name: string;
email: string;
age?: number;
}
// قبل: يجب إرسال كل الخواص الإلزامية
const updateUser: User = { id: "1", name: "Ahmed" }; // ❌ خطأ: email مفقود
// بعد: Partial يحول كل الخواص إلى اختيارية
const partialUpdate: Partial<User> = { id: "1", name: "Ahmed" }; // ✅ صحيح
// خلف الكواليس، Partial<User> تعادل:
type PartialUser = {
id?: string | undefined;
name?: string | undefined;
email?: string | undefined;
age?: number | undefined;
};العكس تماماً هو `Required<T>`، الذي يحول كل الخواص الاختيارية إلى إلزامية. هذا مفيد جداً عندما تعمل مع بيانات تأتي من مصادر خارجية قد تكون غير مكتملة، مثل استجابات API أو قراءات قاعدة البيانات. مثلاً، إذا كان لديك واجهة `Product` فيها بعض الخواص اختيارية، ولكن في جزء معين من التطبيق تحتاج ضمان أن كل الخواص موجودة، يمكنك استخدام `Required<Product>`.
لكن احذر من الفخ الشائع هنا: `Required` لا يضيف قيم افتراضية، بل فقط يجعل TypeScript يرفض الكود إذا كانت الخاصية مفقودة. إذا كنت تتوقع أن الخواص قد تكون `null` أو `undefined`، فعليك استخدام أدوات أخرى مثل `NonNullable` أو التعامل مع القيم المحتملة في الكود نفسه.
في المشاريع الكبيرة، غالباً ما تجد نفسك تعمل مع واجهات ضخمة تحتوي على عشرات الخواص، ولكنك تحتاج فقط جزء صغير منها في مكان معين. مثلاً، واجهة `User` قد تحتوي على معلومات حساسة مثل `passwordHash` و`lastLoginIP`، ولكنك تريد إرسال جزء منها فقط إلى الـ Frontend. هنا يأتي دور `Pick<T, K>` و`Omit<T, K>`، وهما مثل الجراح الذي يقطع بدقة دون أن يلمس ما لا تريد.
`Pick` يسمح لك باختيار مجموعة محددة من الخواص لإنشاء نوع جديد، بينما `Omit` يفعل العكس: يزيل مجموعة محددة من الخواص. في مشروع حقيقي عملت عليه، كان لدينا واجهة `Order` تحتوي على ٢٥ خاصية، ولكن في صفحة عرض الطلبات كنا نحتاج فقط ٥ خواص منها. بدلاً من كتابة واجهة جديدة يدوياً، استخدمنا `Pick<Order, 'id' | 'date' | 'status' | 'total' | 'customerName'>`، مما وفر علينا كتابة ٢٠ سطراً من الكود المكرر.
interface Order {
id: string;
date: Date;
status: 'pending' | 'shipped' | 'delivered' | 'cancelled';
total: number;
customerName: string;
customerEmail: string;
shippingAddress: string;
billingAddress: string;
items: OrderItem[];
paymentMethod: string;
// ... 15 خاصية أخرى
}
// بدلاً من كتابة واجهة جديدة يدوياً:
type OrderPreviewManual = {
id: string;
date: Date;
status: 'pending' | 'shipped' | 'delivered' | 'cancelled';
total: number;
customerName: string;
};
// استخدم Pick:
type OrderPreview = Pick<Order, 'id' | 'date' | 'status' | 'total' | 'customerName'>;
// أو Omit إذا كان عدد الخواص المطلوبة أكثر من الخواص المراد إزالتها:
type OrderPreviewAlt = Omit<Order, 'customerEmail' | 'shippingAddress' | 'billingAddress' | 'items' | 'paymentMethod' /* ... */>;لكن هناك فخ مهم هنا: `Pick` و`Omit` يعملان على مستوى النوع فقط، وليس على مستوى القيم. إذا كان لديك كائن من نوع `Order` وتحتاج تحويله إلى `OrderPreview`، فعليك فعل ذلك يدوياً أو باستخدام مكتبة مثل `lodash/pick`. TypeScript لن يفعل ذلك تلقائياً في وقت التشغيل. مثلاً:
const order: Order = { /* ... */ };
// ❌ خطأ: لا يمكنك تحويل Order إلى OrderPreview تلقائياً
const preview: OrderPreview = order; // TypeScript سيرفض هذا
// ✅ صحيح: يجب عليك اختيار الخواص يدوياً
const preview: OrderPreview = {
id: order.id,
date: order.date,
status: order.status,
total: order.total,
customerName: order.customerName
};
// أو باستخدام مكتبة:
import { pick } from 'lodash';
const preview = pick(order, ['id', 'date', 'status', 'total', 'customerName']);كم مرة وجدت نفسك تعدل على كائن أو مصفوفة ثم اكتشفت بعد ساعة أن هذا التعديل تسبب في خطأ في جزء آخر من التطبيق؟ `Readonly<T>` هو الدرع الذي يحميك من التعديل العرضي على البيانات. عندما تستخدمه، فإن TypeScript سيرفض أي محاولة لتعديل الخواص أو عناصر المصفوفة، مما يجبرك على التفكير مرتين قبل تعديل البيانات الأصلية.
هذا مفيد جداً في حالات مثل: البيانات القادمة من API والتي لا يجب تعديلها، أو الحالات الأولية في React التي يجب عدم تعديلها مباشرة. في مشروع سابق، كنا نتعامل مع بيانات حساسة من قاعدة بيانات، وكان من الضروري عدم تعديلها مباشرة لتجنب الـ Side Effects. استخدمنا `Readonly` على مستوى الواجهة بالكامل، مما منع أي تعديل عرضي على البيانات الأصلية.
interface Config {
apiUrl: string;
timeout: number;
maxRetries: number;
}
// بدون Readonly:
const config: C { apiUrl: "https://api.example.com", timeout: 5000, maxRetries: 3 };
config.timeout = 3000; // ✅ مسموح، لكن قد يسبب مشاكل في أماكن أخرى
// مع Readonly:
const readonlyConfig: Readonly<Config> = { apiUrl: "https://api.example.com", timeout: 5000, maxRetries: 3 };
readonlyConfig.timeout = 3000; // ❌ خطأ: لا يمكن تعديل خاصية 'timeout' لأنها للقراءة فقط
// خلف الكواليس، Readonly<Config> تعادل:
type ReadonlyConfig = {
readonly apiUrl: string;
readonly timeout: number;
readonly maxRetries: number;
};لكن هناك نقطة مهمة: `Readonly` لا يجعل الكائن عميقاً (deep). إذا كان لديك كائن يحتوي على كائنات أخرى أو مصفوفات، فإن `Readonly` سيمنع تعديل الكائن الخارجي فقط، ولكن يمكن تعديل الكائنات الداخلية. إذا كنت تريد حماية عميقة، فعليك استخدام `Readonly` بشكل متكرر أو مكتبة مثل `immer` للتعامل مع البيانات غير القابلة للتعديل.
عندما تعمل مع البيانات الديناميكية، غالباً ما تحتاج إلى خرائط (maps) حيث المفاتيح والقيم من أنواع محددة. مثلاً، تخيل أنك تبني نظام ترجمة، وتحتاج إلى خريطة حيث المفاتيح هي رموز اللغات والقيم هي الترجمات. بدلاً من كتابة النوع يدوياً، `Record<K, T>` يبني لك هذا النوع في سطر واحد.
في مشروع حقيقي، كنا نعمل على نظام إدارة محتوى متعدد اللغات، وكان لدينا واجهة `Content` تحتوي على عدة خواص، كل منها يحتاج إلى ترجمة. استخدمنا `Record` لبناء خرائط للترجمات بسهولة:
interface Content {
title: string;
description: string;
body: string;
}
// بدون Record:
type Translati {
en: Content;
ar: Content;
fr: Content;
es: Content;
};
// مع Record:
type Translations = Record<'en' | 'ar' | 'fr' | 'es', Content>;
// مثال على الاستخدام:
const blogPostTranslations: Translations = {
en: { title: "Utility Types in TypeScript", description: "A practical guide", body: "..." },
ar: { title: "أنواع الأدوات في TypeScript", description: "دليل عملي", body: "..." },
fr: { title: "Les types utilitaires en TypeScript", description: "Un guide pratique", body: "..." },
es: { title: "Tipos de utilidad en TypeScript", description: "Una guía práctica", body: "..." }
};لكن هناك فخ هنا: `Record` يفترض أن كل المفاتيح موجودة. إذا كنت تريد مفاتيح اختيارية، فعليك استخدام `Partial<Record<K, T>>`. مثلاً، إذا كان لديك خريطة للترجمات ولكن ليس كل اللغات متوفرة بعد، يمكنك استخدام:
type PartialTranslati Partial<Record<'en' | 'ar' | 'fr' | 'es', Content>>;
const partialTranslations: PartialTranslations = {
en: { title: "Utility Types", description: "Guide", body: "..." },
ar: { title: "أنواع الأدوات", description: "دليل", body: "..." }
// fr و es غير موجودين، وهذا مسموح لأننا استخدمنا Partial
};في عالم البيانات الحقيقية، غالباً ما تجد نفسك تتعامل مع قيم قد تكون `null` أو `undefined`. `NonNullable<T>` يزيل هذين النوعين من الاتحاد، مما يجبرك على التعامل مع القيم الفعلية فقط. هذا مفيد جداً عندما تكون متأكداً أن القيمة لن تكون فارغة، ولكن TypeScript لا يعرف ذلك بعد.
مثال واقعي: عندما تعمل مع بيانات من قاعدة بيانات، قد تكون بعض الخواص `null`، ولكن في جزء معين من التطبيق أنت متأكد أنها لن تكون فارغة. بدلاً من استخدام `!` للتأكيد القسري، يمكنك استخدام `NonNullable` لجعل TypeScript يفهم ذلك:
interface User {
id: string;
name: string | null;
email: string | null;
}
function sendEmail(user: User) {
// ❌ خطأ: email قد تكون null
console.log(user.email.toLowerCase());
// ✅ صحيح: NonNullable يزيل null من الاتحاد
const email: NonNullable<User['email']> = user.email;
if (email) {
console.log(email.toLowerCase()); // TypeScript يعرف أن email ليست null هنا
}
}`Exclude<T, U>` و`Extract<T, U>` هما أداتان لتصفية الاتحادات (unions). `Exclude` يزيل الأنواع الموجودة في `U` من `T`، بينما `Extract` يحتفظ فقط بالأنواع الموجودة في `U`. مثلاً، إذا كان لديك اتحاد من أنواع الأحداث، وتريد التعامل فقط مع أحداث معينة، يمكنك استخدام `Extract` لتصفية الاتحاد:
type Event =
| { type: 'click'; x: number; y: number }
| { type: 'scroll'; direction: 'up' | 'down' }
| { type: 'keypress'; key: string }
| { type: 'resize'; width: number; height: number };
// استخدم Extract للحصول فقط على أحداث الفأرة:
type MouseEvents = Extract<Event, { type: 'click' | 'scroll' }>;
// MouseEvents تعادل:
// | { type: 'click'; x: number; y: number }
// | { type: 'scroll'; direction: 'up' | 'down' }
// استخدم Exclude لإزالة أحداث الفأرة:
type N Exclude<Event, { type: 'click' | 'scroll' }>;
// NonMouseEvents تعادل:
// | { type: 'keypress'; key: string }
// | { type: 'resize'; width: number; height: number }حتى مع خبرة سنوات في TypeScript، هناك فخاخ شائعة مع Utility Types قد تسبب لك صداعاً لساعات. سأشارك معك بعضاً منها بناءً على أخطائي الشخصية وأخطاء زملائي في مشاريع حقيقية.
كما ذكرت سابقاً، Utility Types لا تغير القيم في وقت التشغيل، بل هي مجرد أداة تحليل ثابت. إذا كان لديك كائن من نوع `User` وتحتاج تحويله إلى `Partial<User>`، فعليك فعل ذلك يدوياً. الكثير من المطورين يعتقدون أن TypeScript سيفعل ذلك تلقائياً، ثم يفاجأون عندما لا يعمل الكود كما يتوقعون.
`Partial<T>` يجعل الخواص اختيارية (`| undefined`)، ولكن لا يجعلها قابلة لأن تكون `null`. إذا كنت تتوقع أن الخواص قد تكون `null`، فعليك استخدام `NonNullable` أو التعامل مع `null` بشكل صريح. مثلاً:
interface User {
name: string | null;
email: string | null;
}
const partialUser: Partial<User> = { name: null }; // ✅ مسموح لأن name اختيارية
// لكن partialUser.email قد تكون undefined، وليس null
// إذا كنت تريد التعامل مع null وundefined معاً:
type NullablePartial<T> = {
[P in keyof T]?: T[P] | null;
};
const nullablePartialUser: NullablePartial<User> = { name: null, email: null }; // ✅ مسموحعندما تستخدم `Exclude` أو `Extract` مع اتحادات كبيرة جداً (مثل اتحاد يحتوي على مئات الأنواع)، قد تلاحظ بطءاً في تحليل TypeScript. هذا لأن TypeScript يحتاج إلى فحص كل نوع في الاتحاد. في مشروع عملت عليه، كان لدينا اتحاد يحتوي على ٣٠٠ نوع من الأحداث، واستخدام `Exclude` عليه تسبب في بطء ملحوظ في الـ IDE. الحل كان تقسيم الاتحاد إلى مجموعات أصغر واستخدام `Exclude` على كل مجموعة على حدة.
`const` يمنع إعادة تعيين المتغير، ولكن لا يمنع تعديل محتوياته إذا كان كائناً أو مصفوفة. `Readonly` يمنع تعديل الخواص أو عناصر المصفوفة، ولكنه لا يمنع إعادة تعيين المتغير إذا لم يكن `const`. مثلاً:
const arr = [1, 2, 3];
arr.push(4); // ✅ مسموح لأن const لا يمنع تعديل المصفوفة
const readonlyArr: ReadonlyArray<number> = [1, 2, 3];
readonlyArr.push(4); // ❌ خطأ: لا يمكن تعديل المصفوفة
let mutableArr: ReadonlyArray<number> = [1, 2, 3];
mutableArr = [4, 5, 6]; // ✅ مسموح لأن المتغير ليس constبعد سنوات من استخدام Utility Types في مشاريع إنتاجية، هذه هي نصائحي الذهبية لك:
Utility Types في TypeScript ليست مجرد ميزة جميلة، بل هي أداة قوية تختصر عليك ساعات من الكود المكرر وتقلل الأخطاء في وقت التطوير. كلما استخدمتهم أكثر، كلما أصبحت أكثر إنتاجية وأكثر ثقة في أنواعك. ابدأ بتطبيقهم في مشروعك الحالي، وستلاحظ الفرق في غضون أيام.
الخطوة التالية؟ جرب كتابة Utility Type مخصص خاص بك. مثلاً، حاول كتابة `DeepReadonly<T>` الذي يجعل كل الخواص والكائنات الداخلية للقراءة فقط. هذا التمرين سيعمق فهمك لكيفية عمل Utility Types خلف الكواليس وسيجعلك مستعداً لمواجهة أي تحدي نوعي في مشاريعك المستقبلية.