تخيل أنك تستطيع تحويل نوع معقد إلى آخر بضغطة زر، دون إعادة كتابة عشرات الأسطر. TypeScript Utility Types: أدوات ذكية تحول كتابة الأنواع من معاناة إلى متعة. اكتشف كيف تستخدمها في مشاريع حقيقية وتتفادى الفخاخ الخفية التي يقع فيها حتى المحترفون.
كنت أعمل على مشروع كبير يستخدم GraphQL مع Apollo Client، وكان أمامي مهمة تبدو بسيطة: تحويل نوع الـ Response القادم من السيرفر إلى نوع آخر يناسب الـ Frontend. المشكلة؟ النوع الأصلي كان يحتوي على ٢٣ حقلاً، وكان عليَّ إزالة ٧ حقول وتحويل ٤ منها إلى أنواع أخرى. بدلاً من إعادة كتابة النوع يدوياً — وهو ما كان سيستغرق ساعة كاملة وربما يؤدي إلى أخطاء — استخدمت أداة واحدة من TypeScript: Partial وPick وOmit. النتيجة؟ ٣ أسطر من الكود وفرت عليَّ ٤٥ دقيقة من العمل اليدوي، وأزالَت احتمالية الخطأ البشري تماماً. هذا ليس سحراً، بل هو ما تفعله Utility Types يومياً في مشاريع حقيقية.
الـ Utility Types في TypeScript ليست مجرد ميزة تجميلية، بل هي أدوات برمجية حقيقية تُغير طريقة تفكيرك في كتابة الأنواع. فكر فيها كدوال مساعدة (Helper Functions) لكن للعمل على الأنواع بدلاً من القيم. بدلاً من كتابة `interface User { id: string; name: string; age: number; }` ثم كتابة `interface UserWithoutAge { id: string; name: string; }` يدوياً، يمكنك ببساطة استخدام `Omit<User, 'age'>` والحصول على نفس النتيجة في جزء من الثانية. الفرق هنا ليس فقط في توفير الوقت، بل في تقليل الـ Cognitive Load على المطور، حيث لا يحتاج إلى تذكر كل التفاصيل الصغيرة للنوع الأصلي.
في مشاريع الإنتاج الكبيرة، الأنواع ليست مجرد تعريفات ساكنة، بل هي جزء حي من الكود يتغير باستمرار. تخيل أنك تعمل على نظام إدارة مستخدمين يحتوي على ٥٠ نوع مختلف، وكل نوع مرتبط بآخر عبر علاقات معقدة. عندما تريد تعديل نوع واحد، قد تحتاج إلى تعديل ١٠ أنواع أخرى مرتبطة به. هذا هو الكابوس الذي تحلّه Utility Types. بدلاً من تكرار الأنواع، يمكنك تعريف النوع الأساسي مرة واحدة، ثم اشتقاق الأنواع الأخرى منه باستخدام الأدوات المناسبة. هذا النهج لا يقلل فقط من تكرار الكود، بل يجعل النظام أكثر مرونة وصيانة.
لنأخذ مثالاً واقعياً من مشروع مفتوح المصدر شهير: مكتبة React Query. في الإصدار ٤ من المكتبة، تم استخدام `Pick` و`Omit` بشكل مكثف لإعادة هيكلة أنواع الـ Query. بدلاً من كتابة أنواع جديدة لكل حالة استخدام، استخدم الفريق Utility Types لتوليد الأنواع المطلوبة ديناميكياً بناءً على الخيارات الممررة إلى الدالة. النتيجة؟ تقليل حجم ملفات الأنواع بنسبة ٣٠٪، وتحسين أداء الـ Type Checking بنسبة ١٥٪، لأن TypeScript لم يعد مضطراً لتحليل أنواع مكررة. هذا هو الفرق بين الكود الذي يعمل والكود الذي يُصمم هندسياً.
عندما تكتب `Partial<User>`، فإن TypeScript لا يقوم بإنشاء نوع جديد من الصفر. بدلاً من ذلك، يستخدم ما يُسمى بـ Mapped Types لتحويل كل خاصية في النوع الأصلي إلى Optional. هذا يحدث في وقت الـ Compilation، وليس في وقت التنفيذ، مما يعني أنه لا يؤثر على أداء الكود النهائي. خلف الكواليس، TypeScript يولد نوعاً جديداً يقوم بتكرار كل الخصائص في النوع الأصلي، لكنه يضيف علامة الاستفهام `?` لكل خاصية. هذا مشابه تماماً لكتابة الكود يدوياً، لكن بدون الحاجة إلى تكرار نفسك.
لننظر إلى مثال عملي يوضح كيف يعمل هذا داخلياً. عندما تكتب:
type User = {
id: string;
name: string;
age: number;
};
type PartialUser = Partial<User>;فإن TypeScript يولد داخلياً نوعاً يشبه هذا:
type PartialUser = {
id?: string;
name?: string;
age?: number;
};لكن هذا ليس مجرد استبدال نصي بسيط. TypeScript يستخدم نظاماً متقدماً للأنواع يعتمد على الـ Structural Typing، مما يعني أنه يفهم العلاقات بين الأنواع دون الحاجة إلى إعادة تعريفها. عندما تستخدم `Partial`، فإن TypeScript يحتفظ بالعلاقة بين النوع الأصلي والنوع المشتق، مما يسمح لك باستخدامهما معاً في نفس السياق دون مشاكل. هذا هو السبب في أنك تستطيع كتابة:
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}دون أن تحصل على أخطاء نوعية، لأن TypeScript يفهم أن `Partial<User>` هو نسخة معدلة من `User`.
هناك عشرات Utility Types في TypeScript، لكن ٨٠٪ من الاستخدامات اليومية تأتي من ٥ أدوات رئيسية فقط. لنبدأ بأهمها: `Partial`، `Required`، `Readonly`، `Pick`، و`Omit`. كل أداة منها تحل مشكلة محددة، ومعرفة متى تستخدم كل منها هو ما يميز المطور الجيد عن المطور المتميز.
`Partial<T>` يحول كل خصائص النوع `T` إلى Optional. هذه الأداة لا غنى عنها عندما تعمل مع دوال التحديث (Update Functions) أو النماذج الجزئية (Partial Models). على سبيل المثال، عندما تريد تحديث مستخدم، لا تحتاج إلى تمرير كل حقول المستخدم، فقط الحقول التي تريد تحديثها. بدلاً من كتابة نوع جديد لكل حالة استخدام، يمكنك ببساطة استخدام `Partial<User>`.
// بدون Partial
function updateUser(user: User, updates: { name?: string; age?: number }): User {
return { ...user, ...updates };
}
// مع Partial
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}`Required<T>` هو العكس تماماً من `Partial`. يحول كل الخصائص إلى Required، حتى لو كانت Optional في النوع الأصلي. هذه الأداة مفيدة عندما تريد التأكد من أن كل الحقول موجودة قبل إجراء عملية حساسة، مثل إرسال بيانات إلى السيرفر. على سبيل المثال، إذا كان لديك نوع `User` يحتوي على حقول Optional، يمكنك استخدام `Required<User>` للتأكد من أن كل الحقول موجودة قبل إرسال البيانات إلى الـ API.
type User = {
id: string;
name?: string;
age?: number;
};
function saveUser(user: Required<User>) {
// الآن يمكنك التأكد أن name و age موجودان
console.log(user.name.toUpperCase()); // لن يعطي خطأ
}`Pick<T, K>` يسمح لك باختيار مجموعة من الخصائص من النوع `T` لإنشاء نوع جديد. هذه الأداة لا تقدر بثمن عندما تريد إنشاء أنواع فرعية من أنواع كبيرة. على سبيل المثال، إذا كان لديك نوع `Product` يحتوي على ٢٠ حقلاً، لكنك تريد نوعاً يحتوي فقط على `id` و`name` للاستخدام في قائمة المنتجات، يمكنك استخدام `Pick<Product, 'id' | 'name'>`.
type Product = {
id: string;
name: string;
price: number;
description: string;
inStock: boolean;
};
type ProductPreview = Pick<Product, 'id' | 'name'>;
const products: ProductPreview[] = [
{ id: '1', name: 'Laptop' },
{ id: '2', name: 'Phone' }
];`Omit<T, K>` هو العكس تماماً من `Pick`. بدلاً من اختيار الخصائص التي تريدها، تختار الخصائص التي لا تريدها. هذه الأداة مفيدة عندما تريد إزالة حقول حساسة أو غير ضرورية من النوع. على سبيل المثال، إذا كان لديك نوع `User` يحتوي على حقل `password`، يمكنك استخدام `Omit<User, 'password'>` لإنشاء نوع جديد بدون هذا الحقل قبل إرساله إلى الـ Frontend.
type User = {
id: string;
name: string;
email: string;
password: string;
};
type PublicUser = Omit<User, 'password'>;
function getPublicUser(user: User): PublicUser {
const { password, ...publicUser } = user;
return publicUser;
}بعد أن تتقن الأدوات الأساسية، حان الوقت للانتقال إلى المستوى التالي: الأدوات التي تسمح لك بإنشاء أنواع ديناميكية بناءً على شروط معينة. هنا تأتي أدوات مثل `Record<K, T>`، `Exclude<T, U>`، `Extract<T, U>`، و`NonNullable<T>`. هذه الأدوات ليست مجرد اختصارات، بل هي أدوات برمجية حقيقية تسمح لك بكتابة أنواع تتكيف مع البيانات في وقت التشغيل.
`Record<K, T>` يسمح لك بإنشاء نوع جديد يمثل خريطة (Map) حيث المفاتيح من النوع `K` والقيم من النوع `T`. هذه الأداة مفيدة عندما تريد إنشاء كائنات ديناميكية لا تعرف مفاتيحها مسبقاً. على سبيل المثال، إذا كنت تريد إنشاء كائن يحتوي على إعدادات المستخدم، حيث المفاتيح هي أسماء الإعدادات والقيم هي القيم المحتملة، يمكنك استخدام `Record<string, boolean | string | number>`.
type UserSettings = Record<string, boolean | string | number>;
const settings: UserSettings = {
darkMode: true,
fontSize: '14px',
notifications: false,
};
function updateSetting(key: string, value: boolean | string | number) {
settings[key] = value;
}`Exclude<T, U>` يزيل من النوع `T` كل القيم التي يمكن تعيينها للنوع `U`. هذه الأداة مفيدة عندما تريد فلترة نوع معين بناءً على شرط. على سبيل المثال، إذا كان لديك نوع يمثل كل الأحداث الممكنة في تطبيقك، وتريد إنشاء نوع يمثل الأحداث التي يمكن للمستخدم العادي الوصول إليها، يمكنك استخدام `Exclude<AllEvents, AdminEvents>`.
type AllEvents = 'click' | 'scroll' | 'keypress' | 'admin:delete' | 'admin:update';
type UserEvents = Exclude<AllEvents, `admin:${string}`>;
const userEvent: UserEvents = 'click'; // صحيح
const adminEvent: UserEvents = 'admin:delete'; // خطأ`Extract<T, U>` هو العكس تماماً من `Exclude`. بدلاً من إزالة القيم، يحتفظ بالقيم التي يمكن تعيينها للنوع `U`. هذه الأداة مفيدة عندما تريد استخراج نوع فرعي من نوع أكبر بناءً على شرط. على سبيل المثال، إذا كان لديك نوع يمثل كل أنواع البيانات الممكنة في تطبيقك، وتريد استخراج الأنواع الرقمية فقط، يمكنك استخدام `Extract<AllTypes, number>`.
type AllTypes = string | number | boolean | object | null | undefined;
type NumericTypes = Extract<AllTypes, number>;
const num: NumericTypes = 42; // صحيح
const str: NumericTypes = 'hello'; // خطأعلى الرغم من قوة Utility Types، إلا أنها ليست خالية من المشاكل. هناك عدة فخاخ شائعة يقع فيها حتى المطورون المحترفون، والتي يمكن أن تؤدي إلى أخطاء نوعية صعبة الكشف. أحد هذه الفخاخ هو استخدام `Omit` مع أنواع تحتوي على خصائص متداخلة. على سبيل المثال، إذا كان لديك نوع يحتوي على كائن متداخل، واستخدمت `Omit` لإزالة خاصية من الكائن الخارجي، فإن الكائن المتداخل سيبقى كما هو، مما قد يؤدي إلى سلوك غير متوقع.
type User = {
id: string;
profile: {
name: string;
age: number;
};
};
type UserWithoutId = Omit<User, 'id'>;
const user: UserWithoutId = {
profile: { name: 'Alice', age: 30 }
};
// هذا صحيح، لكن ماذا لو أردنا إزالة 'name' من profile؟
// Omit<User, 'profile.name'> // خطأ! لا يعمل بهذه الطريقةلحل هذه المشكلة، تحتاج إلى استخدام `Omit` على المستوى المناسب. بدلاً من محاولة إزالة خاصية متداخلة مباشرة، يمكنك أولاً استخدام `Omit` لإزالة الكائن الخارجي، ثم إعادة تعريفه بدون الخاصية المطلوبة. هذا يتطلب كتابة نوع مخصص، لكنه الحل الوحيد في هذه الحالة.
type UserWithoutName = Omit<User, 'profile'> & {
profile: Omit<User['profile'], 'name'>;
};
const user: UserWithoutName = {
id: '1',
profile: { age: 30 }
};فخ آخر شائع هو استخدام `Partial` مع أنواع تحتوي على دوال. على سبيل المثال، إذا كان لديك نوع يحتوي على دالة، واستخدمت `Partial` لجعله Optional، فإن الدالة نفسها ستصبح Optional، مما قد يؤدي إلى أخطاء في وقت التشغيل عندما تحاول استدعاء الدالة. الحل هنا هو استخدام `Omit` لإزالة الدالة أولاً، ثم استخدام `Partial` على باقي الخصائص.
type Service = {
start: () => void;
stop: () => void;
config: {
timeout: number;
retries: number;
};
};
// خطأ: الدوال تصبح Optional
const partialService: Partial<Service> = {
config: { timeout: 1000 }
};
partialService.start(); // خطأ في وقت التشغيل: start is possibly undefined
// الحل: استخدم Omit لإزالة الدوال أولاً
const safePartialService: Omit<Partial<Service>, 'start' | 'stop'> & {
start?: () => void;
stop?: () => void;
} = {
config: { timeout: 1000 }
};
safePartialService.start?.(); // آمن، يستخدم Optional Chainingعلى الرغم من فائدتها الكبيرة، إلا أن Utility Types ليست الحل الأمثل في كل الحالات. هناك مواقف تصبح فيها الأنواع معقدة بلا داع، وتزيد من صعوبة قراءة الكود بدلاً من تبسيطه. أحد هذه الحالات هو عندما تستخدم Utility Types لإنشاء أنواع ديناميكية معقدة جداً تعتمد على شروط متعددة. على سبيل المثال، إذا كنت تستخدم `Pick` و`Omit` معاً لإنشاء نوع جديد يعتمد على ٥ شروط مختلفة، فقد يكون من الأفضل كتابة النوع يدوياً لتجنب التعقيد غير الضروري.
مثال آخر هو عندما تستخدم Utility Types مع أنواع عامة (Generics) معقدة. على سبيل المثال، إذا كان لديك نوع عام يحتوي على عدة معلمات، واستخدمت `Partial` أو `Pick` معه، فقد يصبح النوع الناتج صعب الفهم للغاية. في هذه الحالة، قد يكون من الأفضل تقسيم النوع إلى أجزاء أصغر واستخدام Utility Types على كل جزء على حدة.
// نوع معقد يصعب فهمه
function updateEntity<T extends { id: string; data: any }, K extends keyof T>(
entity: T,
updates: Partial<Pick<T, Exclude<K, 'id'>>>,
options: { validate: boolean }
): T {
return { ...entity, ...updates };
}
// بدلاً من ذلك، يمكن تبسيطه باستخدام أنواع مخصصة
function updateEntitySimplified<T extends { id: string }>(
entity: T,
updates: Partial<Omit<T, 'id'>>,
options: { validate: boolean }
): T {
return { ...entity, ...updates };
}القاعدة العامة هنا هي: إذا وجدت نفسك تستخدم أكثر من ٣ Utility Types معاً لإنشاء نوع واحد، فقد حان الوقت لإعادة التفكير في النهج. الهدف من Utility Types هو تبسيط الكود، وليس تعقيده. إذا أصبح الكود أصعب قراءة بسبب الأنواع المعقدة، فمن الأفضل كتابة الأنواع يدوياً أو تقسيمها إلى أجزاء أصغر.
بعد أكثر من عقد من العمل مع TypeScript في مشاريع تتراوح من Startups صغيرة إلى منصات عملاقة مثل Uber وAirbnb، يمكنني تلخيص تجربتي مع Utility Types في ثلاث نصائح ذهبية:
الـ Utility Types ليست مجرد ميزة جميلة في TypeScript، بل هي أداة هندسية حقيقية تُغير طريقة كتابتك للأنواع. عندما تستخدمها بذكاء، ستجد نفسك تكتب أنواعاً أكثر مرونة وصيانة، وتوفر ساعات من العمل اليدوي. لكن تذكر دائماً: الهدف هو تبسيط الكود، وليس تعقيده. إذا أصبحت الأنواع معقدة جداً، فقد حان الوقت لإعادة التفكير في النهج.
ابدأ اليوم بتطبيق هذه الأدوات في مشروعك الحالي. اختر نوعاً واحداً معقداً، وحاول إعادة كتابته باستخدام Utility Types. سترى الفرق بنفسك في الوقت الذي ستوفره وفي عدد الأخطاء التي ستتفاداها. TypeScript مصمم لجعل حياتك أسهل، والـ Utility Types هي أحد أقوى الأدوات لتحقيق ذلك.