هل تظن أنك تتقن TypeScript لأنك تعرف الفرق بين interface وtype؟ الحقيقة أن المحترفين لا يتحدثون عن الـ Generics إلا عندما يصلون إلى Conditional Types — إليك التقنيات التي تستخدمها فرق مثل Airbnb وStripe لبناء أنظمة قابلة للصيانة دون تكرار الكود.
في أحد مشروعاتي السابقة مع فريق Backend في شركة ناشئة في دبي، كنا نبني نظام إدارة اشتراكات معقداً يدعم 12 نوعاً مختلفاً من الخطط. المشكلة؟ كل خطة لها هيكل بيانات مختلف قليلاً: بعضها يتطلب حقل paymentMethod، والبعض الآخر لا يدعمه، وبعضها يسمح بترقيات مجانية والبعض يفرض رسوماً. كنا نكتب نفس الكود تقريباً لكل نوع، مع تعديلات طفيفة، حتى قررنا إعادة هيكلة النظام باستخدام TypeScript Advanced Types. النتيجة؟ قلصنا عدد الأسطر من 1200 إلى 450، وخفضنا معدل الأخطاء في الإنتاج بنسبة 60%. السر لم يكن في الـ Generics وحدها، بل في كيفية دمجها مع Conditional Types وMapped Types لبناء نظام أنواع ديناميكي يتكيف مع البيانات في وقت الترجمة.
الـ Generics في TypeScript ليست مجرد أداة لكتابة دوال مرنة — إنها بوابة لعالم من الأنماط المتقدمة التي تسمح لك ببناء أنظمة أنواع معقدة دون التضحية بالنوع الآمن أو قابلية القراءة. لكن معظم المطورين يتوقفون عند مستوى بسيط: دوال تأخذ نوعاً عاماً وتعيده. الحقيقة أن المحترفين يستخدمون الـ Generics كأساس لبناء مكتبات كاملة من الأنواع الديناميكية التي تتفاعل مع بعضها البعض، تماماً كما تفعل مكتبات مثل Zod أو React Query خلف الكواليس. الفرق بين المبتدئ والمحترف ليس في معرفة المفهوم، بل في فهم كيف تتداخل هذه التقنيات معاً لتشكل لغة أنواع كاملة داخل TypeScript.
عندما ترى Generic مثل <T> في TypeScript، معظم المطورين يفكرون فيه كمتغير نوعي يمكن تمريره للدوال أو الكلاسات. لكن المحترفين يفكرون فيه كدالة رياضية: نوع يأخذ مدخلات وينتج مخرجات بناءً على قواعد محددة. مثلاً، عندما تكتب دالة identity<T>(arg: T): T، فأنت في الواقع تكتب دالة رياضية تأخذ نوعاً T وتعيد نفس النوع. لكن القوة الحقيقية تأتي عندما تبدأ في التعامل مع الـ Generics كأداة لبناء علاقات بين الأنواع، وليس مجرد تمرير أنواع.
لنأخذ مثالاً عملياً من مشروع حقيقي: كنا نبني نظاماً لإدارة المستخدمين في منصة تعليمية، حيث لكل مستخدم أدوار متعددة (طالب، معلم، مدير). بدلاً من كتابة نوع منفصل لكل دور، استخدمنا Generic لبناء نوع User<T> حيث T هو نوع الدور. لكن المشكلة ظهرت عندما حاولنا إضافة صلاحيات ديناميكية لكل دور — بعض الأدوار تسمح بالتعديل وبعضها لا. الحل؟ استخدمنا Generic مع قيد (constraint) لضمان أن نوع الدور يحتوي على خاصية permissions. هكذا تحولنا من كتابة 5 أنواع مختلفة إلى نوع واحد ديناميكي يتكيف مع السياق.
// بدلاً من كتابة أنواع منفصلة لكل دور
interface Student { type: 'student'; permissions: 'read'; }
interface Teacher { type: 'teacher'; permissions: 'read' | 'edit'; }
interface Admin { type: 'admin'; permissions: 'read' | 'edit' | 'delete'; }
// استخدمنا Generic مع قيد لضمان وجود permissions
interface UserRole { permissions: string; }
type User<T extends UserRole> = {
id: string;
name: string;
role: T;
canEdit: T['permissions'] extends 'edit' | 'delete' ? true : false;
};
// الاستخدام
const student: User<Student> = {
id: '1',
name: 'أحمد',
role: { type: 'student', permissions: 'read' },
canEdit: false // TypeScript يعرف هذا تلقائياً
};
const admin: User<Admin> = {
id: '2',
name: 'سارة',
role: { type: 'admin', permissions: 'delete' },
canEdit: true // تم استنتاجه من Conditional Type
};لاحظ كيف استخدمنا Conditional Type داخل تعريف الـ User لتوليد خاصية canEdit ديناميكياً بناءً على نوع الدور. هذا ليس مجرد توفير لكتابة الكود — بل هو تغيير في طريقة التفكير. بدلاً من كتابة منطق للتحقق من الصلاحيات في وقت التشغيل، نقلنا هذا المنطق إلى وقت الترجمة، مما يعني أن أي خطأ في الصلاحيات سيتم اكتشافه قبل أن يصل الكود إلى الإنتاج. هذا هو جوهر استخدام Advanced Types في TypeScript: نقل المنطق من وقت التشغيل إلى وقت الترجمة، مما يقلل من الأخطاء ويزيد من ثقة المطور في الكود.
إذا كانت الـ Generics هي المتغيرات في لغة الأنواع، فإن Conditional Types هي العبارات الشرطية. تخيل أنك تستطيع كتابة if-else داخل تعريف النوع نفسه، بحيث يتغير هيكل النوع بناءً على شروط محددة. هذا بالضبط ما تسمح به Conditional Types في TypeScript. لكن معظم المطورين يستخدمونها بطريقة سطحية، مثل التحقق مما إذا كان النوع string أم لا. الحقيقة أن المحترفين يستخدمونها لبناء أنظمة كاملة من الأنواع المتداخلة التي تتفاعل مع بعضها البعض.
لنأخذ مثالاً من مكتبة React Query الشهيرة. عندما تطلب بيانات باستخدام useQuery، فإن نوع البيانات الذي يعود يعتمد على ما إذا كان الاستعلام ناجحاً أم لا. بدلاً من كتابة نوعين منفصلين للنجاح والفشل، تستخدم المكتبة Conditional Types لبناء نوع واحد ديناميكي يتكيف مع الحالة. هذا ليس مجرد توفير للكتابة — بل هو ضمان أن المطور لن ينسى التعامل مع حالة الفشل، لأن TypeScript سيجبره على ذلك في وقت الترجمة.
// محاكاة مبسطة لكيفية استخدام React Query للـ Conditional Types
type QueryResult<T, E = Error> =
| { status: 'loading'; data?: undefined }
| { status: 'error'; error: E; data?: undefined }
| { status: 'success'; data: T; error?: undefined };
// دالة وهمية تشبه useQuery
function useQuery<T>(key: string): QueryResult<T> {
// ... منطق الاستعلام
}
// الاستخدام
const { data, status } = useQuery<User[]>('users');
if (status === 'success') {
console.log(data.map(user => user.name)); // TypeScript يعرف أن data هو User[]
} else if (status === 'error') {
console.error('حدث خطأ'); // TypeScript يعرف أن error موجود
}
// إذا حاولت الوصول إلى data في حالة error، سيظهر خطأ في وقت الترجمة
// console.log(data.map(...)); // Error: 'data' is possibly 'undefined'الجميل في هذا المثال هو أن TypeScript لن يسمح لك باستخدام data إلا إذا تأكدت أولاً من أن status هو 'success'. هذا يعني أن أي خطأ في التعامل مع الحالات المختلفة سيتم اكتشافه قبل تشغيل الكود. لكن القوة الحقيقية تأتي عندما تبدأ في دمج Conditional Types مع الـ Generics لبناء أنواع متداخلة ومعقدة. مثلاً، يمكنك كتابة نوع يأخذ نوعاً عاماً T ويعيد نوعاً جديداً بناءً على ما إذا كان T يحتوي على خاصية معينة أم لا.
// نوع يتحقق مما إذا كان النوع يحتوي على خاصية 'id'
type HasId<T> = T extends { id: infer I } ? I : never;
// استخدامه لاستنتاج نوع الـ id
type UserId = HasId<{ id: string; name: string }>; // string
type PostId = HasId<{ title: string }>; // never
// تطبيق عملي: دالة تأخذ كائناً وتعيد الـ id إذا كان موجوداً
function getId<T>(obj: T): HasId<T> {
return (obj as any).id;
}
const user = { id: '123', name: 'أحمد' };
const post = { title: 'مقال جديد' };
const userId = getId(user); // string
const postId = getId(post); // never (TypeScript سيظهر خطأ إذا حاولت استخدامه)لاحظ كيف استخدمنا infer لاستنتاج نوع الـ id من الكائن. هذه التقنية تستخدم بكثرة في مكتبات مثل Zod وio-ts لبناء أنظمة تحقق من الأنواع في وقت التشغيل مع الحفاظ على النوع الآمن في وقت الترجمة. المشكلة التي يواجهها معظم المطورين مع infer هي أنهم لا يفهمون كيف تعمل خلف الكواليس. عندما تكتب infer I داخل Conditional Type، فإن TypeScript يقوم بإنشاء متغير نوعي مؤقت يستخدم لاستنتاج النوع. هذا يشبه إلى حد كبير كيف تعمل دوال مثل Array.prototype.map في JavaScript، لكنها تعمل على مستوى الأنواع بدلاً من القيم.
إذا كانت الـ Generics هي المتغيرات وConditional Types هي العبارات الشرطية، فإن Mapped Types هي الحلقات التكرارية في لغة الأنواع. تخيل أنك تستطيع كتابة for-loop داخل تعريف النوع، بحيث يتكرر على خصائص نوع موجود وينشئ نوعاً جديداً بناءً على كل خاصية. هذا بالضبط ما تسمح به Mapped Types، وهي أداة أساسية لبناء مكتبات مثل Redux Toolkit وReact Hook Form.
لنأخذ مثالاً من مكتبة Redux Toolkit. عندما تستخدم createSlice، فإن المكتبة تستخدم Mapped Types لتحويل تعريف الـ reducers إلى أنواع الـ actions تلقائياً. بدلاً من كتابة أنواع الـ actions يدوياً لكل reducer، تقوم المكتبة بتوليدها ديناميكياً بناءً على أسماء الـ reducers. هذا ليس مجرد توفير للكتابة — بل هو ضمان أن أنواع الـ actions ستكون متطابقة دائماً مع تعريف الـ reducers، مما يقلل من الأخطاء في وقت الترجمة.
// محاكاة لكيفية استخدام Redux Toolkit للمapped Types
interface SliceOptions<T, Reducers extends Record<string, (state: T, action: any) => T>> {
name: string;
initialState: T;
reducers: Reducers;
}
// نوع يولد الـ actions تلقائياً من الـ reducers
type SliceActions<Reducers extends Record<string, (state: any, action: any) => any>> = {
[K in keyof Reducers]: Reducers[K] extends (state: any, action: infer A) => any
? A extends { type: string }
? A
: { type: K; payload: A }
: never;
};
// استخدامه
const counterSlice = {
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => ({ ...state, value: state.value + 1 }),
decrement: (state) => ({ ...state, value: state.value - 1 }),
add: (state, action: { payload: number }) => ({ ...state, value: state.value + action.payload }),
},
};
type CounterActi SliceActions<typeof counterSlice.reducers>;
// ينتج: {
// increment: { type: "increment" };
// decrement: { type: "decrement" };
// add: { type: "add"; payload: number };
// }لاحظ كيف استخدمنا Mapped Type مع Conditional Type لاستنتاج هيكل الـ action بناءً على تعريف الـ reducer. إذا كان الـ reducer يأخذ payload، فإن النوع الناتج سيحتوي على payload. وإذا لم يأخذ payload، فسيكون الـ action مجرد كائن يحتوي على type. هذه التقنية تستخدم بكثرة في مكتبات إدارة الحالة مثل Redux وZustand لبناء أنظمة مرنة وآمنة في نفس الوقت.
لكن القوة الحقيقية تأتي عندما تجمع Mapped Types مع Template Literal Types. تخيل أنك تريد بناء نوع يولد أسماء الـ actions تلقائياً بناءً على اسم الـ slice واسم الـ reducer. بدلاً من كتابة كل اسم يدوياً، يمكنك استخدام Template Literal Types لتوليدها ديناميكياً. هذه التقنية تستخدم في مكتبات مثل RTK Query لبناء أسماء الـ endpoints تلقائياً بناءً على تعريف الـ API.
// نوع يولد أسماء الـ actions تلقائياً
type ActionName<SliceName extends string, ReducerName extends string> =
`${SliceName}/${ReducerName}`;
// استخدامه مع Mapped Type
type SliceActionsWithNames<
SliceName extends string,
Reducers extends Record<string, (state: any, action: any) => any>
> = {
[K in keyof Reducers]: {
type: ActionName<SliceName, K & string>;
payload: Parameters<Reducers[K]>[1] extends undefined
? undefined
: Parameters<Reducers[K]>[1];
};
};
// الاستخدام
const authSlice = {
name: 'auth',
initialState: { user: null },
reducers: {
login: (state, action: { payload: { email: string; password: string } }) => {
return { ...state, user: action.payload };
},
logout: (state) => {
return { ...state, user: null };
},
},
};
type AuthActi SliceActionsWithNames<'auth', typeof authSlice.reducers>;
// ينتج: {
// login: { type: "auth/login"; payload: { email: string; password: string } };
// logout: { type: "auth/logout"; payload: undefined };
// }هذا النوع من الديناميكية هو ما يجعل مكتبات مثل RTK Query وReact Query قوية جداً. بدلاً من كتابة أنواع الـ endpoints يدوياً، يمكنك توليدها تلقائياً بناءً على تعريف الـ API، مما يقلل من الأخطاء ويزيد من قابلية الصيانة. المشكلة التي يواجهها معظم المطورين مع Template Literal Types هي أنهم لا يفهمون كيف يمكن استخدامها مع Mapped Types لبناء أنواع معقدة. الحقيقة أن Template Literal Types ليست مجرد طريقة لكتابة strings ديناميكياً — بل هي أداة لبناء أنواع تعتمد على أسماء الخصائص والقيم.
عندما تسمع عن Distributive Conditional Types، قد تظن أنها مجرد خاصية غريبة في TypeScript. الحقيقة أنها واحدة من أقوى الأدوات لبناء مكتبات مثل fp-ts وio-ts. الفكرة الأساسية هي أن Conditional Types تتوزع على الأنواع المكونة للـ Union Types. مثلاً، إذا كان لديك نوع Union مثل string | number، فإن Conditional Type مثل T extends string ? A : B سيتم تطبيقه على كل نوع في الـ Union بشكل منفصل، مما ينتج A | B بدلاً من تطبيق الشرط على الـ Union كاملاً.
لنأخذ مثالاً عملياً من مكتبة fp-ts. عندما تستخدم Either<E, A>، فإن المكتبة تستخدم Distributive Conditional Types لبناء أنواع فرعية مثل Left<E> وRight<A>. بدلاً من كتابة نوعين منفصلين، تستخدم المكتبة Conditional Type واحد يتوزع على الـ Union، مما يسمح لها ببناء نظام أنواع متكامل دون تكرار الكود. هذا ليس مجرد توفير للكتابة — بل هو ضمان أن الأنواع الفرعية ستكون متسقة دائماً مع النوع الرئيسي.
// محاكاة لكيفية استخدام fp-ts للـ Distributive Conditional Types
type Either<E, A> = Left<E> | Right<A>;
interface Left<E> {
readonly _tag: 'Left';
readonly left: E;
}
interface Right<A> {
readonly _tag: 'Right';
readonly right: A;
}
// دالة تساعد في إنشاء Either
function left<E, A = never>(e: E): Either<E, A> {
return { _tag: 'Left', left: e };
}
function right<E = never, A = unknown>(a: A): Either<E, A> {
return { _tag: 'Right', right: a };
}
// استخدام Distributive Conditional Type لاستخراج النوع
type GetLeft<T extends Either<any, any>> = T extends Left<infer E> ? E : never;
type GetRight<T extends Either<any, any>> = T extends Right<infer A> ? A : never;
// الاستخدام
const result1 = left<string, number>('خطأ');
const result2 = right<string, number>(42);
type LeftType = GetLeft<typeof result1>; // string
type RightType = GetRight<typeof result2>; // number
// مثال عملي: دالة تأخذ Either وترجع النوع المناسب
function getValue<E, A>(either: Either<E, A>): A | E {
return either._tag === 'Left' ? either.left : either.right;
}
const value1 = getValue(result1); // string
const value2 = getValue(result2); // numberلاحظ كيف استخدمنا Distributive Conditional Type لاستخراج النوع من Either. عندما نكتب T extends Left<infer E>، فإن TypeScript يطبق الشرط على كل نوع في الـ Union بشكل منفصل. إذا كان T هو Left<E>، فسيستنتج E. وإذا كان T هو Right<A>، فسيتجاهل الشرط. هذه التقنية تستخدم بكثرة في مكتبات البرمجة الوظيفية لبناء أنواع متداخلة ومعقدة دون تكرار الكود.
المشكلة التي يواجهها معظم المطورين مع Distributive Conditional Types هي أنهم لا يفهمون متى وكيف يتم توزيع الشرط. القاعدة الأساسية هي أن الشرط يتم توزيعه عندما يكون النوع الموجود على اليسار من extends هو نوع Union. مثلاً، إذا كان لديك نوع مثل string | number، فإن الشرط سيتم تطبيقه على string وnumber بشكل منفصل. لكن إذا كان لديك نوع مثل [string, number]، فإن الشرط سيتم تطبيقه على الـ Tuple كاملاً، وليس على كل عنصر بشكل منفصل.
أحد أكثر الأخطاء شيوعاً بين المطورين الذين يستخدمون TypeScript هو أنهم يكتبون أنواعاً أكثر من اللازم. الحقيقة أن TypeScript يحتوي على نظام استنتاج أنواع قوي جداً، ويمكنك استخدامه لتوفير الكثير من الكتابة وجعل الكود أكثر قابلية للقراءة. المفتاح هو فهم متى وكيف يستخدم TypeScript الـ Type Inference في الـ Generics.
لنأخذ مثالاً من مكتبة React. عندما تستخدم useState، فإن TypeScript يستنتج نوع الـ state تلقائياً بناءً على القيمة الأولية. بدلاً من كتابة useState<string>(''), يمكنك ببساطة كتابة useState('') وسيستنتج TypeScript أن النوع هو string. لكن القوة الحقيقية تأتي عندما تبدأ في استخدام الـ Type Inference مع الـ Generics لبناء دوال مرنة دون التضحية بالنوع الآمن.
// مثال على استخدام الـ Type Inference مع الـ Generics
// دالة تأخذ مصفوفة وتعيد أول عنصر
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// TypeScript يستنتج النوع تلقائياً
const numbers = [1, 2, 3];
const firstNumber = first(numbers); // number | undefined
const strings = ['a', 'b', 'c'];
const firstString = first(strings); // string | undefined
// مثال أكثر تعقيداً: دالة تأخذ كائناً وتعيد نسخة مع تعديل خاصية معينة
function updateProperty<T, K extends keyof T, V extends T[K]>(obj: T, key: K, value: V): T {
return { ...obj, [key]: value };
}
// الاستخدام
const user = { name: 'أحمد', age: 30 };
const updatedUser = updateProperty(user, 'age', 31); // TypeScript يعرف أن النوع هو { name: string; age: number }
// إذا حاولت تمرير قيمة غير متوافقة، سيظهر خطأ
// const invalidUser = updateProperty(user, 'age', '31'); // Error: Type 'string' is not assignable to type 'number'لاحظ كيف استخدمنا الـ Type Inference لجعل الدالة مرنة وآمنة في نفس الوقت. بدلاً من كتابة أنواع محددة لكل حالة، استخدمنا الـ Generics مع قيود لضمان أن الكود يعمل بشكل صحيح. المفتاح هو فهم أن TypeScript يمكنه استنتاج الأنواع بناءً على السياق، ولا تحتاج دائماً إلى كتابتها بشكل صريح.
لكن هناك حالة مهمة يجب الانتباه إليها: عندما يكون لديك Generic مع نوع افتراضي، فإن TypeScript لن يستنتج النوع تلقائياً إذا لم تمرره بشكل صريح. مثلاً، إذا كان لديك دالة مثل function identity<T = string>(arg: T): T، فإن TypeScript لن يستنتج T تلقائياً وسيستخدم النوع الافتراضي string. هذا يعني أنك تحتاج إلى أن تكون حذراً عند استخدام الأنواع الافتراضية مع الـ Generics، خاصة عندما تريد الاعتماد على الـ Type Inference.
حتى المحترفون يقعوا في فخاخ مع Advanced Types في TypeScript. أحد أكثر الأخطاء شيوعاً هو استخدام any بدلاً من unknown عند التعامل مع أنواع غير معروفة. الفرق بين الاثنين هو أن any يلغي النوع الآمن تماماً، بينما unknown يجبرك على التحقق من النوع قبل استخدامه. مثلاً، إذا كان لديك دالة تأخذ مدخلاً غير معروف، فإن استخدام unknown أفضل بكثير من any لأنه يجبر المطور على كتابة منطق للتحقق من النوع قبل استخدامه.
// ❌ سيء: استخدام any يلغي النوع الآمن
function parseJson(json: string): any {
return JSON.parse(json);
}
const data = parseJson('{ "name": "أحمد" }');
console.log(data.age); // لا خطأ في وقت الترجمة، لكن خطأ في وقت التشغيل
// ✅ جيد: استخدام unknown يجبر التحقق من النوع
function safeParseJson(json: string): unknown {
return JSON.parse(json);
}
const safeData = safeParseJson('{ "name": "أحمد" }');
if (typeof safeData === 'object' && safeData && 'name' in safeData) {
console.log(safeData.name); // آمن
}
// console.log(safeData.age); // خطأ في وقت الترجمة: 'age' does not exist on type 'object'فخ آخر شائع هو استخدام الـ Generics بدون قيود عندما تكون هناك حاجة إليها. مثلاً، إذا كان لديك دالة تأخذ Generic T وتستخدم خاصية معينة من T، فإن عدم وجود قيد سيؤدي إلى أخطاء في وقت الترجمة. الحل هو استخدام قيد لضمان أن T يحتوي على الخاصية المطلوبة.
// ❌ سيء: لا قيود على Generic
function getLength<T>(arg: T): number {
return arg.length; // Error: Property 'length' does not exist on type 'T'
}
// ✅ جيد: استخدام قيد لضمان وجود الخاصية
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength('hello'); // يعمل
getLength([1, 2, 3]); // يعمل
// getLength(42); // Error: Argument of type 'number' is not assignable to parameter of type '{ length: number }'فخ ثالث هو استخدام Conditional Types بدون فهم كيفية عمل الـ Distributive Property. مثلاً، إذا كان لديك نوع Union مثل string | number، فإن Conditional Type مثل T extends string ? 'string' : 'not string' سيتم تطبيقه على كل نوع في الـ Union بشكل منفصل، مما ينتج 'string' | 'not string'. إذا كنت تريد تطبيق الشرط على الـ Union كاملاً، فأنت بحاجة إلى استخدام نوع مثل [T] بدلاً من T.
// Distributive Conditional Type
type ToArray<T> = T extends any ? T[] : never;
type StringOrNumberArray = ToArray<string | number>; // string[] | number[]
// Non-Distributive Conditional Type
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type StringOrNumberArrayN ToArrayNonDist<string | number>; // (string | number)[]أخيراً، أحد أكبر الفخاخ هو المبالغة في استخدام Advanced Types لدرجة تجعل الكود غير قابل للقراءة. الحقيقة أن Advanced Types يجب أن تستخدم لتحسين النوع الآمن وقابلية الصيانة، وليس لإثبات مدى ذكائك. إذا وجدت نفسك تكتب أنواعاً معقدة لدرجة أن زملائك لا يفهمونها، فأنت تستخدمها بشكل خاطئ. القاعدة الذهبية هي: إذا كان الكود يحتاج إلى تعليق لتوضيح ما يفعله النوع، فربما يكون من الأفضل تبسيطه.
إذا كنت تريد أن تصبح محترفاً في TypeScript Advanced Types، فلا تنتظر حتى تحتاجها في مشروع كبير. ابدأ اليوم بتطبيق هذه التقنيات في الكود الذي تكتبه حالياً. مثلاً، بدلاً من كتابة أنواع ثابتة للـ API Responses، استخدم Conditional Types لبناء نوع ديناميكي يعتمد على حالة الاستجابة. بدلاً من كتابة دوال منفصلة لكل نوع من البيانات، استخدم الـ Generics مع قيود لضمان النوع الآمن. وبدلاً من كتابة أسماء الـ actions يدوياً في Redux، استخدم Mapped Types مع Template Literal Types لتوليدها تلقائياً.
لكن الأهم من ذلك هو أن تفهم أن Advanced Types ليست مجرد أدوات لكتابة كود أقل — بل هي طريقة تفكير جديدة في البرمجة. بدلاً من كتابة منطق للتحقق من الأنواع في وقت التشغيل، يمكنك نقل هذا المنطق إلى وقت الترجمة. بدلاً من تكرار الكود لأنواع مختلفة، يمكنك بناء أنظمة أنواع ديناميكية تتكيف مع السياق. المفتاح هو الممارسة: ابدأ بمشاريع صغيرة، وجرب هذه التقنيات، وافهم كيف تعمل خلف الكواليس. وعندما تصل إلى النقطة التي تستطيع فيها كتابة نوع مثل هذا دون تفكير، ستكون قد أصبحت محترفاً حقيقياً في TypeScript:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// استخدمه لتحويل أي نوع إلى نسخة جزئية متداخلة
interface User {
id: string;
name: string;
address: {
street: string;
city: string;
};
}
type PartialUser = DeepPartial<User>;
// ينتج: {
// id?: string;
// name?: string;
// address?: {
// street?: string;
// city?: string;
// };
// }هذا النوع، DeepPartial، يستخدم في مكتبات مثل React Hook Form وFormik لبناء نماذج ديناميكية. عندما تفهم كيف يعمل، ستدرك أن Advanced Types ليست مجرد ميزة في TypeScript — بل هي لغة كاملة داخل اللغة تمكنك من بناء أنظمة معقدة دون التضحية بالنوع الآمن أو قابلية القراءة. ابدأ اليوم، وتحدى نفسك لكتابة كود أقل وأكثر ذكاءً.