توقف عن إعادة كتابة نفس الـ Interfaces مراراً وتكراراً. اكتشف كيف تحول TypeScript من أداة تدقيق إلى معمل هندسة أنواع ذكي يوفر ساعات من العمل عبر Utility Types مثل Partial وPick وOmit، مع أمثلة حقيقية من مشاريع الإنتاج.
كنت أعمل على واجهة إدارة مستخدمين في لوحة تحكم SaaS، وكان الـ Backend يرسل بيانات المستخدم في ثلاث أشكال مختلفة: عند إنشاء الحساب يرسل كل الحقول، عند تحديثه يرسل فقط الحقول المعدلة، وعند عرض القائمة يرسل ملخصاً مختصراً. بدلاً من كتابة ثلاثة Interfaces متطابقة تقريباً، استخدمت Utility Types لتحويل Interface واحد إلى ثلاثة أشكال مختلفة في سطرين فقط. هذا ليس مجرد توفير وقت، بل هو تغيير في طريقة التفكير: TypeScript لم يعد مجرد أداة تدقيق، بل أصبح معملاً هندسياً للأypes يمكنك فيه توليد الأنواع التي تحتاجها بدلاً من كتابتها يدوياً.
الـ Utility Types في TypeScript هي دوال مدمجة في اللغة تأخذ نوعاً واحداً أو أكثر وتعيد نوعاً جديداً مشتقاً منها. فكر فيها كعمليات Map وFilter وReduce ولكن للعمل على الأنواع بدلاً من المصفوفات. عندما تستخدم Partial<User> مثلاً، أنت لا تعدل الكائن نفسه، بل تطلب من الـ Compiler أن يعيد بناء نوع جديد حيث جميع خصائص User تصبح اختيارية. هذا يحدث في وقت الترجمة فقط، ولا يضيف أي حمل على وقت التشغيل، مما يجعله حلاً مثالياً للأداء.
في عالم الـ APIs، غالباً ما تحتاج إلى إرسال جزء فقط من الكائن الكامل. مثلاً، عند تحديث ملف شخصي، لا ترسل كل بيانات المستخدم، بل فقط الحقول التي تغيرت. هنا يأتي دور Partial. بدلاً من كتابة interface جديد أو استخدام union types مع undefined لكل خاصية، يمكنك ببساطة تحويل كل الخصائص إلى اختيارية بسطر واحد. لكن احذر: Partial لا يعني أن الكائن سيكون دائماً كاملاً، بل يعني أن الـ Compiler لن يشتكي إذا نسيت بعض الخصائص. هذا قد يؤدي إلى أخطاء في وقت التشغيل إذا لم تتعامل مع القيم المفقودة بشكل صحيح.
في مشروع سابق، كنا نستخدم Partial<User> في دالة updateUser التي تستقبل object يحتوي فقط على الحقول المعدلة. المشكلة ظهرت عندما أضفنا خاصية جديدة إلى User interface ولم نحدث الدوال التي تستخدم Partial. الـ Compiler لم يشتكي لأن Partial يسمح بكل الخصائص بأن تكون مفقودة، لكن في وقت التشغيل كانت البيانات الجديدة تضيع ببساطة. الحل كان استخدام Partial مع Pick بدلاً من Partial الكامل، مما يجبرنا على تحديد بالضبط أي الخصائص يمكن تحديثها.
// قبل: كتابة interface جديد لكل حالة
interface User {
id: string;
name: string;
email: string;
age?: number;
}
interface UserUpdate {
name?: string;
email?: string;
age?: number;
}
// بعد: استخدام Partial لتحويل الكل إلى اختياري
function updateUser(userId: string, updates: Partial<User>) {
// منطق التحديث
}
// مشكلة محتملة: Partial يسمح بكل شيء بأن يكون مفقوداً
updateUser("123", {}); // لا خطأ في وقت الترجمة، لكن ربما خطأ في وقت التشغيل
// الحل الأفضل: Partial مع Pick للتحكم الدقيق
function safeUpdateUser(userId: string, updates: Partial<Pick<User, 'name' | 'email'>>) {
// الآن فقط name و email يمكن تحديثهما
}عندما تريد استخراج جزء محدد من نوع كبير، Pick هو الأداة المناسبة. تخيل أن لديك نوع Product يحتوي على ٣٠ خاصية، لكن في صفحة قائمة المنتجات تريد عرض خمسة فقط: id, name, price, image, rating. بدلاً من كتابة interface جديد أو استخدام object destructuring في كل مكان، يمكنك استخدام Pick لاستخراج هذه الخصائص فقط. هذا ليس مجرد توفير وقت، بل هو أيضاً تحسين للأداء في وقت الترجمة، حيث أن TypeScript سيعرف بالضبط أي الخصائص مسموح بها في هذا السياق.
Omit هو العكس تماماً: بدلاً من اختيار الخصائص التي تريدها، تختار الخصائص التي لا تريدها. هذا مفيد جداً في حالات الأمان، مثلاً عندما تريد إرسال بيانات المستخدم إلى الـ Frontend ولكن بدون الحقول الحساسة مثل password أو token. في مشروع لشركة fintech، كنا نستخدم Omit<User, 'password' | 'ssn'> في كل الـ API responses التي تعرض بيانات المستخدم. هذا يضمن أننا لن ننسى أبداً إزالة الحقول الحساسة، حتى لو أضفنا حقولاً جديدة إلى User interface لاحقاً.
// نوع كبير يحتوي على الكثير من الخصائص
interface Product {
id: string;
name: string;
price: number;
description: string;
category: string;
stock: number;
images: string[];
rating: number;
createdAt: Date;
updatedAt: Date;
metadata: Record<string, unknown>;
}
// استخدام Pick لاستخراج الخصائص المطلوبة للقائمة
function renderProductList(products: Pick<Product, 'id' | 'name' | 'price' | 'image' | 'rating'>[]) {
// منطق العرض
}
// استخدام Omit لإزالة البيانات الحساسة
interface User {
id: string;
name: string;
email: string;
password: string;
token: string;
ssn: string; // رقم الضمان الاجتماعي
}
// في الـ API response
function getUserProfile(userId: string): Omit<User, 'password' | 'token' | 'ssn'> {
const user = database.getUser(userId);
return {
id: user.id,
name: user.name,
email: user.email
};
}قد تعتقد أن Pick وOmit متشابهان، ولكنهما يعملان بطرق مختلفة تماماً خلف الكواليس. عندما تستخدم Pick، الـ Compiler ينشئ نوعاً جديداً يحتوي فقط على الخصائص التي اخترتها، مما يقلل حجم النوع في الذاكرة. أما Omit، فهو أكثر تعقيداً: الـ Compiler ينشئ نوعاً جديداً يحتوي على كل الخصائص الأصلية باستثناء تلك التي حذفتها، مما يعني أن حجم النوع يظل كبيراً نسبياً. هذا الفرق يصبح مهماً عندما تعمل مع أنواع تحتوي على مئات الخصائص، مثل تلك الموجودة في أنظمة ERP الكبيرة.
في مشروع لشركة لوجستية، كنا نعمل مع نوع Shipment يحتوي على ١٢٠ خاصية. عند استخدام Pick لاستخراج ١٠ خصائص فقط، لاحظنا تحسناً ملحوظاً في سرعة الـ Type Checking، حيث أن الـ Compiler كان يتعامل مع أنواع أصغر. أما عند استخدام Omit لإزالة ١٠ خصائص فقط، فلم يكن هناك فرق ملحوظ في الأداء، لأن النوع الناتج كان لا يزال كبيراً. القاعدة الذهبية هنا: استخدم Pick عندما تريد عدداً صغيراً من الخصائص، واستخدم Omit عندما تريد إزالة عدد صغير من الخصائص من نوع كبير.
في كثير من الأحيان تحتاج إلى تخزين بيانات في شكل key-value pairs، حيث تكون المفاتيح من نوع محدد والقيم من نوع آخر. مثلاً، تخزين إعدادات المستخدم حيث المفاتيح هي أسماء الإعدادات والقيم هي القيم الموافقة. بدلاً من استخدام object عام أو Map، يمكنك استخدام Record لتحديد بالضبط أنواع المفاتيح والقيم. هذا لا يضيف فقط أماناً في وقت الترجمة، بل يجعل الكود أكثر وضوحاً لأي مطور يقرأه لاحقاً.
في مشروع لوحة تحكم لإدارة المحتوى، كنا نستخدم Record<string, boolean> لتخزين حالة الأزرار في الواجهة. مثلاً، { isEditing: true, isSaving: false, isPublishing: false }. هذا يجعل الكود أكثر أماناً من استخدام object عادي، حيث أن الـ Compiler سيشتكي إذا حاولت إضافة قيمة ليست boolean. كما أنه أكثر مرونة من استخدام enum، خاصة عندما لا تعرف مسبقاً كل المفاتيح الممكنة.
// قبل: استخدام object عام بدون تحديد الأنواع
const userSettings: { [key: string]: any } = {
theme: 'dark',
notifications: true,
language: 'ar'
};
// المشكلة: يمكن إضافة أي نوع من القيم
userSettings.theme = 123; // لا خطأ في وقت الترجمة
// بعد: استخدام Record لتحديد الأنواع
const strictUserSettings: Record<string, string | boolean> = {
theme: 'dark',
notifications: true,
language: 'ar'
};
// الآن الـ Compiler سيشتكي إذا حاولت إضافة نوع غير مسموح
strictUserSettings.theme = 123; // خطأ: Type 'number' is not assignable to type 'string | boolean'
// مثال أكثر دقة: تحديد المفاتيح المسموح بها
const validSettings = ['theme', 'notifications', 'language'] as const;
type SettingKey = typeof validSettings[number]; // 'theme' | 'notifications' | 'language'
const preciseUserSettings: Record<SettingKey, string | boolean> = {
theme: 'dark',
notifications: true,
language: 'ar'
};
// الآن لا يمكن إضافة مفاتيح غير مسموح بها
preciseUserSettings.f '14px'; // خطأ: Property 'fontSize' does not exist on typeعندما تعمل مع دوال عالية الرتبة (Higher-Order Functions) أو مكتبات مثل Redux وReact، غالباً ما تحتاج إلى معرفة نوع القيمة التي تعيدها دالة معينة أو أنواع المعاملات التي تتقبلها. بدلاً من كتابة هذه الأنواع يدوياً، يمكنك استخدام ReturnType وParameters لاستخراجها مباشرة من توقيع الدالة. هذا مفيد جداً في الحالات التي تريد فيها إنشاء دالة جديدة لها نفس توقيع دالة موجودة، أو عندما تريد التحقق من توافق أنواع الدوال في وقت الترجمة.
في مشروع يستخدم Redux، كنا نحتاج إلى إنشاء Middleware جديد يتوافق مع توقيع Middleware القياسي. بدلاً من كتابة النوع يدوياً، استخدمنا ReturnType وParameters لاستخراجه من الدالة createStore. هذا يضمن أن Middleware الخاص بنا سيكون متوافقاً دائماً مع أحدث إصدار من Redux، حتى لو تغير توقيع الدالة في المستقبل. كما استخدمنا نفس التقنية لإنشاء دوال مساعدة في اختبارات الوحدة، حيث أردنا محاكاة دوال حقيقية بنفس توقيعها.
// استخراج نوع القيمة التي تعيدها دالة
function fetchUser(userId: string): Promise<{ id: string; name: string }> {
return Promise.resolve({ id: userId, name: "John Doe" });
}
type UserType = ReturnType<typeof fetchUser>; // Promise<{ id: string; name: string }>
type UserData = Awaited<ReturnType<typeof fetchUser>>; // { id: string; name: string }
// استخراج أنواع معاملات الدالة
function updateProfile(name: string, age: number, email?: string) {
// منطق التحديث
}
type UpdateProfileParams = Parameters<typeof updateProfile>;
// [name: string, age: number, email?: string | undefined]
// استخدام في دوال عالية الرتبة
function createLogger<F extends (...args: any[]) => any>(fn: F): (...args: Parameters<F>) => void {
return (...args) => {
console.log(`Calling ${fn.name} with`, args);
fn(...args);
};
}
const loggedUpdateProfile = createLogger(updateProfile);
loggedUpdateProfile("John", 30); // يعمل بشكل صحيح
// loggedUpdateProfile("John"); // خطأ: Expected 2-3 arguments, but got 1الجمال الحقيقي في Utility Types يظهر عندما تبدأ في ترکیبها معاً لإنشاء أنواع معقدة من أنواع بسيطة. مثلاً، يمكنك استخدام Pick مع Partial لإنشاء نوع حيث بعض الخصائص مطلوبة وبعضها اختياري. أو استخدام Omit مع Record لإنشاء قاموس يستبعد بعض المفاتيح. هذا المستوى من التحكم يجعل TypeScript أداة هندسة حقيقية، حيث يمكنك بناء أنواع دقيقة جداً تناسب احتياجاتك بدقة.
في مشروع لوحة تحكم لإدارة المحتوى، كنا نحتاج إلى نوع يمثل حالة التحرير للصفحة، حيث بعض الحقول مطلوبة وبعضها اختياري، وبعض الحقول لا يجب أن تكون موجودة على الإطلاق. بدلاً من كتابة هذا النوع يدوياً، استخدمنا ترکیب من Pick وPartial وOmit لإنشائه تلقائياً من نوع Page الأساسي. هذا جعل الكود أكثر قابلية للصيانة، حيث أن أي تغيير في نوع Page سينعكس تلقائياً في نوع التحرير، دون الحاجة إلى تحديثه يدوياً.
// نوع أساسي للصفحة
interface Page {
id: string;
title: string;
content: string;
slug: string;
published: boolean;
createdAt: Date;
updatedAt: Date;
metadata: Record<string, unknown>;
}
// نوع التحرير: بعض الحقول مطلوبة، بعضها اختياري، وبعضها ممنوع
// - title و content مطلوبة
// - slug اختياري (يمكن توليده تلقائياً)
// - الحقول الأخرى ممنوعة
type PageEditable = Pick<Page, 'title' | 'content'> &
Partial<Pick<Page, 'slug'>> &
Omit<Page, 'id' | 'published' | 'createdAt' | 'updatedAt' | 'metadata'>;
// استخدام في مكون التحرير
function PageEditor({ page }: { page: PageEditable }) {
// منطق التحرير
}
// مثال آخر: إنشاء نوع للـ API request
interface User {
id: string;
name: string;
email: string;
password: string;
role: 'admin' | 'editor' | 'viewer';
lastLogin: Date;
}
// نوع التسجيل: name و email و password مطلوبة، role اختياري، الحقول الأخرى ممنوعة
type SignupRequest = Pick<User, 'name' | 'email' | 'password'> &
Partial<Pick<User, 'role'>> &
Omit<User, 'id' | 'lastLogin'>;
function signup(userData: SignupRequest) {
// منطق التسجيل
}
// مثال متقدم: ترکیب مع Conditional Types
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
variants?: Array<{
color: string;
size: string;
price: number;
}>;
}
// نوع المنتج للواجهة: إذا كان المنتج في المخزون، اعرض السعر، وإلا اعرض "Sold Out"
type ProductUI = Omit<Product, 'price'> & {
price: Product['inStock'] extends true ? number : 'Sold Out';
};
function renderProduct(product: ProductUI) {
// منطق العرض
}رغم قوة Utility Types، إلا أنها تأتي مع مجموعة من الفخاخ التي يمكن أن تؤدي إلى أخطاء غريبة في وقت الترجمة أو حتى في وقت التشغيل. أحد أكثر الأخطاء شيوعاً هو استخدام Partial على أنواع تحتوي على دوال أو getters، حيث أن Partial يجعل كل الخصائص اختيارية، بما في ذلك تلك التي قد تكون مطلوبة لتنفيذ الدوال بشكل صحيح. مثلاً، إذا كان لديك نوع يحتوي على دالة تعتمد على خاصية معينة، فإن Partial قد يجعل هذه الخاصية مفقودة، مما يؤدي إلى خطأ في وقت التشغيل.
فخ آخر هو استخدام Utility Types مع أنواع تحتوي على دوائر مرجعية (Circular References). مثلاً، إذا كان لديك نوع Category يحتوي على مصفوفة من Products، وكل Product يحتوي على مرجع إلى Category، فإن استخدام Utility Types مثل DeepPartial قد يؤدي إلى أخطاء في الـ Compiler أو حتى تجميده. في مثل هذه الحالات، من الأفضل تجنب Utility Types المتقدمة والاعتماد على أنواع مبسطة أو كتابة الأنواع يدوياً.
// فخ Partial مع الدوال
interface Counter {
count: number;
increment: () => void;
}
function createCounter(): Counter {
let count = 0;
return {
count,
increment: () => { count++; }
};
}
// استخدام Partial قد يكسر الدالة
const partialCounter: Partial<Counter> = {
increment: createCounter().increment
};
partialCounter.increment(); // خطأ في وقت التشغيل: Cannot read property 'count' of undefined
// فخ Awaited
async function getData() {
return { id: 1, name: "Test" };
}
type DataType = Awaited<typeof getData>; // Promise<{ id: number; name: string }>
// لا، الصحيح هو:
type CorrectDataType = Awaited<ReturnType<typeof getData>>; // { id: number; name: string }
// فخ الدوائر المرجعية
interface Category {
id: string;
name: string;
products: Product[];
}
interface Product {
id: string;
name: string;
category: Category;
}
// استخدام DeepPartial قد يؤدي إلى مشاكل
// type DeepPartial<T> = {
// [P in keyof T]?: DeepPartial<T[P]>;
// };
// const partialCategory: DeepPartial<Category> = {
// products: [{}] // قد يتسبب في أخطاء أو تجميد
// };