هل تعتقد أنك تتقن TypeScript لأنك تعرف الفرق بين interface وtype؟ الحقيقة أن 90% من المطورين يتوقفون عند المستوى السطحي. هنا نكشف لك تقنيات الـ Advanced Types التي يستخدمها المحترفون فقط لبناء مكتبات قوية مثل React وZod — من Generic Types التي تتنفس مع الكود إلى Conditional Types التي تتخذ قرارات في
في أحد المشاريع الكبيرة التي عملت عليها مع فريق مكون من 12 مطوراً، واجهنا مشكلة غريبة: السيرفر كان يرسل استجابات صحيحة تماماً من حيث البيانات، لكن الـ Frontend كان يتعطل عشوائياً. بعد أيام من الـ Debugging، اكتشفنا أن المشكلة ليست في الـ Logic بل في الـ Types. كان الـ TypeScript يسمح بمرور أنواع غير متوقعة داخل الـ Generic Function، مما أدى إلى كسر الـ State في أماكن لم نتوقعها أصلاً. هذه اللحظة كانت بداية رحلتي الحقيقية مع الـ Advanced Types — ليس كتركيبة جميلة للكود، بل كسلاح لحماية النظام من نفسه.
الـ TypeScript ليس مجرد أداة لإضافة أنواع ثابتة للكود، بل هو لغة برمجة كاملة تعمل في وقت الترجمة. عندما تتقن الـ Advanced Types، تتحول من مجرد مستخدم للأنواع إلى مهندس يصمم نظاماً كاملاً من الأنواع يتفاعل مع بعضها البعض. المشكلة أن معظم المطورين يتوقفون عند مستوى الـ Basic Generics وUnion Types، بينما الحقيقة أن الـ TypeScript قادر على أشياء مذهلة مثل اتخاذ قرارات منطقية في وقت الترجمة، وتحليل الأنماط داخل الأنواع، وحتى تنفيذ عمليات رياضية على الـ Types نفسها. في هذا المقال، سنغوص عميقاً في عالم الـ Advanced Types، بدءاً من الـ Generic Types التي تتنفس مع الكود، ووصولاً إلى الـ Conditional Types التي تتخذ قرارات في وقت الترجمة.
الـ Generic Types هي أول تقنية يتعرف عليها المطورون عند الانتقال من الـ Basic Types إلى المستوى المتقدم في TypeScript. لكن معظمهم يستخدمونها بشكل سطحي جداً، كأنها مجرد متغير يمكن تمريره للنوع. الحقيقة أن الـ Generics في TypeScript هي أداة قوية تسمح لك ببناء أنواع ديناميكية تتفاعل مع الكود بشكل ذكي. المشكلة الأكبر التي أراها في الكود اليوم هي أن المطورين يستخدمون الـ Generics كحاويات جامدة، بدلاً من استخدامها كأدوات ديناميكية تتكيف مع السياق.
لنأخذ مثالاً واقعياً: عندما تعمل مع مكتبات مثل React أو Redux، غالباً ما ترى أنواعاً مثل `useState<T>` أو `createSlice<State>`. هذه الأنواع ليست مجرد قوالب جامدة، بل هي أنظمة ذكية تتفاعل مع الكود الذي تكتبه. مثلاً، عندما تكتب `useState<number>(0)`، فإن الـ TypeScript لا يحفظ فقط أن القيمة من نوع `number`، بل يفهم أيضاً أن الـ Setter Function يجب أن تقبل فقط قيم من نوع `number`. هذا التفاعل الديناميكي هو ما يجعل الـ Generics قوية حقاً.
// مثال بسيط لكن قوي على Generic Function
function identity<T>(arg: T): T {
return arg;
}
// هنا TypeScript يفهم أن T هو string
const output = identity<string>("hello");
// output: string
// لكن الأهم هو عندما لا نحدد النوع صراحةً
const inferredOutput = identity(42);
// inferredOutput: number — TypeScript استنتج النوع تلقائياً
// مثال أكثر تعقيداً: Generic مع Constraints
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// هذا الكود يعمل مع أي نوع له خاصية length
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
// getLength(42); // خطأ: Type 'number' is not assignable to type '{ length: number }'
// مثال من العالم الحقيقي: Generic مع Default Type
interface ApiResponse<T = any> {
data: T;
status: number;
message?: string;
}
// الآن يمكننا استخدام ApiResponse بدون تحديد النوع
const defaultResponse: ApiResp { data: {}, status: 200 };
// أو تحديد نوع محدد
const userResponse: ApiResponse<{ id: string; name: string }> = {
data: { id: "1", name: "Ahmed" },
status: 200
};الـ Constraints في الـ Generics هي ما يجعلها حقاً قوية. بدونها، تكون الـ Generics مجرد حاويات فارغة. لكن عندما تضيف قيوداً مثل `T extends { length: number }`، فإنك تحول الـ Generic من أداة سلبية إلى أداة نشطة تفهم السياق. هذا بالضبط ما تفعله مكتبات مثل Axios عندما تحدد أن الـ Response يجب أن يحتوي على خاصية `data`.
إذا كنت تعمل في مشروع كبير، فأنت بالتأكيد واجهت مشكلة تكرار الأنواع. مثلاً، لديك نوع `User` يحتوي على 20 خاصية، وتحتاج إلى إنشاء نوع جديد يحتوي على نفس الخصائص لكن كلها اختيارية. الحل التقليدي هو كتابة نوع جديد يدوياً، لكن هذا يؤدي إلى تكرار الكود وصعوبة الصيانة. هنا تأتي الـ Mapped Types لتنقذك. هذه التقنية تسمح لك بتحويل نوع موجود إلى نوع جديد بناءً على قواعد محددة، تماماً كما تفعل الـ Array.map مع المصفوفات.
الـ Mapped Types في TypeScript هي أداة قوية تسمح لك بإنشاء أنواع جديدة بناءً على أنواع موجودة. الفكرة الأساسية هي أنك تأخذ نوعاً موجوداً، وتطبق عليه عملية تحويل، للحصول على نوع جديد. هذا مفيد جداً عندما تريد إنشاء أنواع متشابهة لكنها تختلف في بعض الخصائص، مثل جعل جميع الخصائص اختيارية، أو تحويل جميع الخصائص إلى نوع آخر.
// مثال على Mapped Type يجعل جميع الخصائص اختيارية
interface User {
id: string;
name: string;
email: string;
age: number;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
// PartialUser هو نفس نوع User لكن جميع الخصائص اختيارية
// هذا بالضبط ما يفعله الـ Utility Type المدمج Partial<T>
type PartialUser2 = Partial<User>;
// مثال أكثر تعقيداً: تحويل جميع الخصائص إلى boolean
interface ValidationResult {
isValid: boolean;
errors: string[];
}
type UserValidation = {
[K in keyof User]: ValidationResult;
};
// UserValidation سيكون نوعاً يحتوي على نفس مفاتيح User لكن القيم من نوع ValidationResult
// مثال من العالم الحقيقي: تحويل نوع API إلى نوع Frontend
interface ApiUser {
id: number;
full_name: string;
email_address: string;
date_of_birth: string;
}
type Fr {
[K in keyof ApiUser as K extends `full_${infer Rest}` ? `full${Capitalize<Rest>}`
: K extends `email_${infer Rest}` ? `email${Capitalize<Rest>}`
: K extends `date_${infer Rest}` ? `date${Capitalize<Rest>}`
: K]: ApiUser[K];
};
/* FrontendUser سيكون:
{
id: number;
fullName: string;
emailAddress: string;
dateOfBirth: string;
}
*/الـ Mapped Types مع الـ Key Remapping هي ما يجعل هذه التقنية حقاً قوية. في المثال الأخير، استخدمنا `as` لإعادة تسمية المفاتيح بناءً على نمط معين. هذا بالضبط ما تفعله مكتبات مثل GraphQL Code Generator عندما تحول الأنواع من Schema إلى أنواع TypeScript. الفكرة هي أنك تستطيع تحويل الأسماء بناءً على قواعد محددة، مما يسمح لك بإنشاء أنواع تتوافق مع معايير الكود الخاصة بك دون الحاجة إلى كتابة كل شيء يدوياً.
إذا كنت تعتقد أن الـ Types في TypeScript هي مجرد قوالب جامدة، فأنت على وشك اكتشاف عالم جديد تماماً. الـ Conditional Types تسمح لك بكتابة منطق يتخذ قرارات في وقت الترجمة بناءً على الأنواع. فكر فيها كأنها `if` statements لكن تعمل على مستوى الأنواع. هذه التقنية هي ما يجعل مكتبات مثل Zod وTypeORM قوية جداً، لأنها تسمح للكود بأن يتكيف مع الأنواع ديناميكياً دون الحاجة إلى كتابة كود متكرر.
الـ Conditional Types في TypeScript تتبع الصيغة `T extends U ? X : Y`. هذا يعني: إذا كان النوع T يمتد من النوع U، استخدم النوع X، وإلا استخدم النوع Y. لكن القوة الحقيقية تأتي عندما تجمع هذه التقنية مع الـ Infer Keyword، التي تسمح لك باستخراج أنواع فرعية من أنواع معقدة. مثلاً، يمكنك كتابة نوع يستخرج نوع العنصر من مصفوفة، أو نوع القيمة من Promise، أو حتى نوع الخصائص من نوع معين.
// مثال بسيط على Conditional Type
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
// مثال أكثر تعقيداً: استخراج نوع العنصر من المصفوفة
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type StringElement = ArrayElement<string[]>; // string
// مثال من العالم الحقيقي: استخراج نوع القيمة من Promise
async function fetchData(): Promise<{ id: string; name: string }> {
return { id: "1", name: "Ahmed" };
}
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type DataType = UnwrapPromise<ReturnType<typeof fetchData>>;
// DataType: { id: string; name: string }
// مثال متقدم: Conditional Type مع Recursion
interface ApiResponse<T> {
data: T;
status: number;
pagination?: {
next?: string;
prev?: string;
};
}
type FlattenApiResponse<T> = T extends ApiResponse<infer U>
? U extends object
? FlattenApiResponse<U> & { status: T["status"] }
: { data: U; status: T["status"] }
: T;
// استخدام مع نوع معقد
interface PaginatedUser {
id: string;
name: string;
posts: ApiResponse<{ id: string; title: string }[]>;
}
type FlattenedUser = FlattenApiResponse<ApiResponse<PaginatedUser>>;
/* FlattenedUser سيكون:
{
id: string;
name: string;
posts: {
id: string;
title: string;
}[];
status: number;
}
*/الـ Recursive Conditional Types هي ما يجعل هذه التقنية حقاً مذهلة. في المثال الأخير، استخدمنا نوعاً متكرراً لفك جميع الطبقات المتداخلة من الـ ApiResponse. هذا بالضبط ما تفعله مكتبات مثل React Query عندما تحول أنواع الـ Nested Data إلى أنواع مسطحة يسهل التعامل معها. المشكلة التي يواجهها معظم المطورين مع الـ Conditional Types هي أنهم يحاولون استخدامها لحل مشاكل بسيطة، بينما قوتها الحقيقية تظهر عندما تتعامل مع أنواع معقدة ومتداخلة.
هناك جانب خفي في الـ Conditional Types لا يعرفه الكثيرون: عندما تستخدمها مع Union Types، فإنها تتوزع على كل عنصر في الاتحاد. مثلاً، إذا كان لديك نوع `A | B` وتمريره إلى Conditional Type، فإن TypeScript سيطبق الشرط على كل نوع في الاتحاد بشكل منفصل. هذه الخاصية تسمى Distributive Conditional Types، وهي ما يسمح لك بكتابة أنواع ذكية تتعامل مع مجموعات من الأنواع دفعة واحدة.
// مثال على Distributive Conditional Types
// بدون توزيع
type ToArray<T> = T[];
type Arr1 = ToArray<string | number>; // (string | number)[]
// مع توزيع
// لاحظ أننا نضع T في قوسين لإجبار التوزيع
// بدون القوسين، لن يكون التوزيع فعالاً
type ToArrayDistributive<T> = T extends any ? T[] : never;
type Arr2 = ToArrayDistributive<string | number>; // string[] | number[]
// مثال عملي: فلترة أنواع معينة من Union
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
type FilterByKind<T, K> = T extends { kind: K } ? T : never;
type Circles FilterByKind<Shape, "circle">; // Circle
// هذا مفيد جداً في مكتبات مثل Redux Toolkit عندما تريد فلترة Actions
// مثال متقدم: تحويل جميع الخصائص من نوع معين إلى نوع آخر
interface User {
id: string;
name: string;
age: number;
isActive: boolean;
}
type ConvertProperties<T, From, To> = {
[K in keyof T]: T[K] extends From ? To : T[K];
};
type StringifiedUser = ConvertProperties<User, number | boolean, string>;
/* StringifiedUser سيكون:
{
id: string;
name: string;
age: string;
isActive: string;
}
*/الـ Distributive Conditional Types هي ما يسمح لك بكتابة أنواع تتعامل مع مجموعات من الأنواع بكفاءة. في المثال الأخير، استخدمنا هذه التقنية لتحويل جميع الخصائص من نوع معين إلى نوع آخر. هذا بالضبط ما تفعله مكتبات مثل Prisma عندما تحول أنواع الـ Database إلى أنواع TypeScript. الفكرة هي أنك تستطيع كتابة نوع واحد يتعامل مع مجموعة كاملة من الأنواع، بدلاً من كتابة نوع منفصل لكل حالة.
إذا كنت تعتقد أن التعامل مع النصوص في TypeScript يقتصر على `string` و`string[]`، فأنت تفوت فرصة مذهلة. الـ Template Literal Types تسمح لك بمعالجة النصوص على مستوى الأنواع، مما يفتح الباب أمام إمكانيات لم تكن ممكنة من قبل. مثلاً، يمكنك كتابة أنواع تتحقق من تنسيق الـ URL، أو تتحقق من نمط معين في الـ String، أو حتى تولد أنواع جديدة بناءً على النصوص. هذه التقنية هي ما تجعل مكتبات مثل Zod وYup قادرة على التحقق من تنسيقات البيانات المعقدة في وقت الترجمة.
الـ Template Literal Types في TypeScript تسمح لك بإنشاء أنواع جديدة بناءً على النصوص. الفكرة الأساسية هي أنك تستطيع استخدام نفس الصيغة التي تستخدمها في الـ Template Literals في الكود العادي، لكن على مستوى الأنواع. مثلاً، يمكنك كتابة نوع مثل `` `user_${string}` `` الذي يمثل جميع النصوص التي تبدأ بـ `user_`. لكن القوة الحقيقية تأتي عندما تجمع هذه التقنية مع الـ Conditional Types وMapped Types لإنشاء أنواع ذكية تتفاعل مع النصوص ديناميكياً.
// مثال بسيط على Template Literal Type
// نوع يمثل جميع IDs التي تبدأ بـ "user_"
type UserId = `user_${string}`;
function getUser(id: UserId) {
// ...
}
getUser("user_123"); // صحيح
// getUser("post_123"); // خطأ: Argument of type '"post_123"' is not assignable to parameter of type '`user_${string}`'
// مثال أكثر تعقيداً: استخراج أجزاء من النص
// نوع يستخرج ID من نص مثل "user_123"
type ExtractId<T extends string> = T extends `user_${infer U}` ? U : never;
type Id = ExtractId<"user_123">; // "123"
// مثال من العالم الحقيقي: التحقق من تنسيق الـ URL
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiEndpoint = `/api/${string}`;
type ApiCall = `${HttpMethod} ${ApiEndpoint}`;
function makeApiCall(method: HttpMethod, endpoint: ApiEndpoint): void {
console.log(`${method} ${endpoint}`);
}
// هذا الكود صحيح
makeApiCall("GET", "/api/users");
// هذا الكود خطأ لأن endpoint لا يبدأ بـ "/api/"
// makeApiCall("POST", "/users"); // خطأ
// مثال متقدم: توليد أنواع جديدة بناءً على النصوص
interface EventMap {
click: { x: number; y: number };
hover: { element: HTMLElement };
keypress: { key: string };
}
type EventName = keyof EventMap;
type EventHandlerName<T extends EventName> = `on${Capitalize<T>}`;
type Handlers = {
[K in EventName as EventHandlerName<K>]: (event: EventMap[K]) => void;
};
/* Handlers سيكون:
{
onClick: (event: { x: number; y: number }) => void;
onHover: (event: { element: HTMLElement }) => void;
onKeypress: (event: { key: string }) => void;
}
*/الـ Template Literal Types مع الـ Key Remapping هي ما يجعل هذه التقنية حقاً قوية. في المثال الأخير، استخدمنا هذه التقنية لتوليد أسماء الـ Event Handlers بناءً على أسماء الأحداث. هذا بالضبط ما تفعله مكتبات مثل React عندما تولد أنواع الـ Props تلقائياً من أسماء الـ Events. الفكرة هي أنك تستطيع كتابة أنواع تتفاعل مع النصوص ديناميكياً، مما يسمح لك بإنشاء واجهات برمجية أكثر مرونة وقابلة للتوسع.
إذا كنت تكتب أنواعاً يدوياً لكل متغير وكل دالة في الكود، فأنت تفعل شيئاً خاطئاً. الـ TypeScript لديه نظام استدلال أنواع قوي جداً يمكنه تخمين الأنواع بناءً على السياق. المشكلة أن معظم المطورين لا يفهمون كيف يعمل هذا النظام، مما يؤدي إلى كتابة أنواع زائدة أو غير دقيقة. عندما تتقن الـ Type Inference، يمكنك كتابة كود أنظف وأكثر مرونة، مع الحفاظ على أمان الأنواع.
الـ Type Inference في TypeScript هو ما يجعل اللغة سهلة الاستخدام دون التضحية بالأمان. الفكرة الأساسية هي أن الـ TypeScript يحاول تخمين النوع بناءً على السياق. مثلاً، عندما تكتب `const x = 42`، فإن TypeScript يستنتج أن `x` من نوع `42` (Literal Type)، وليس مجرد `number`. هذا الاستدلال الذكي هو ما يسمح لك بكتابة كود أقل مع الحفاظ على أمان الأنواع. لكن القوة الحقيقية تأتي عندما تفهم متى تسمح لـ TypeScript بالاستدلال، ومتى تتدخل لتحديد النوع يدوياً.
// مثال بسيط على Type Inference
const x = 42; // TypeScript يستنتج أن x من نوع 42 (Literal Type)
const y = [1, 2, 3]; // TypeScript يستنتج أن y من نوع number[]
// مثال على استدلال الأنواع في الدوال
function add(a: number, b: number) {
return a + b;
}
// TypeScript يستنتج أن return type هو number
// مثال متقدم: استدلال الأنواع مع Generics
function identity<T>(arg: T): T {
return arg;
}
// هنا TypeScript يستنتج أن T هو string
const str = identity("hello"); // str: string
// مثال من العالم الحقيقي: استدلال الأنواع مع الـ Context
interface User {
id: string;
name: string;
}
function getUserName(user: User) {
return user.name;
}
// هنا TypeScript يستنتج أن return type هو string
const name = getUserName({ id: "1", name: "Ahmed" }); // name: string
// مثال على متى تتدخل لتحديد النوع يدوياً
// بدون تحديد النوع، TypeScript يستنتج أن arr من نوع number[]
const arr = [1, 2, 3];
// لكن إذا أردنا أن يكون النوع أكثر تحديداً، نتدخل
const tuple: [number, number, number] = [1, 2, 3];
// مثال متقدم: استدلال الأنواع مع الـ Mapped Types
interface ApiResponse<T> {
data: T;
status: number;
}
function createResponse<T>(data: T): ApiResponse<T> {
return { data, status: 200 };
}
// هنا TypeScript يستنتج أن return type هو ApiResponse<{ id: string }>
const resp createResponse({ id: "1" });
// response: ApiResponse<{ id: string }>
// مثال على مشكلة شائعة مع Type Inference
function parseJson(json: string) {
return JSON.parse(json); // TypeScript يستنتج أن return type هو any
}
// الحل: تحديد النوع يدوياً
function safeParseJson<T>(json: string): T {
return JSON.parse(json);
}
// الآن يمكننا تحديد النوع عند الاستدعاء
const user = safeParseJson<User>('{"id": "1", "name": "Ahmed"}');
// user: Userالمشكلة الأكبر التي أراها في الكود اليوم هي أن المطورين إما يكتبون أنواعاً زائدة دون داعٍ، أو يعتمدون كلياً على الـ Type Inference دون فهم حدوده. الحقيقة هي أن الـ Type Inference في TypeScript قوي جداً، لكنه ليس سحرياً. مثلاً، عندما تستخدم `JSON.parse`، فإن TypeScript لا يستطيع تخمين النوع الناتج، لذلك يعطيه نوع `any`. في هذه الحالة، يجب عليك التدخل وتحديد النوع يدوياً. المفتاح هو أن تفهم متى تسمح لـ TypeScript بالاستدلال، ومتى تتدخل لتحديد النوع بدقة.
بعد أكثر من عشر سنوات في كتابة TypeScript، أستطيع أن أقول بثقة: الـ Advanced Types ليست مجرد ميزة جميلة للكود، بل هي سلاح لحماية النظام من الأخطاء قبل أن تحدث. المشكلة أن معظم المطورين يتعلمون هذه التقنيات بشكل سطحي، ثم ينسونها بعد أسبوع. الحقيقة هي أن إتقان الـ Advanced Types يتطلب ممارسة مستمرة وفهم عميق لكيفية عمل TypeScript خلف الكواليس.
إذا أردت أن تصبح سيداً في الـ Advanced Types، فاتبع هذه النصائح العملية: أولاً، ابدأ بتطبيق الـ Generic Types في كل مكان في الكود، حتى في الدوال البسيطة. لا تنتظر حتى تحتاجها في مشروع كبير، بل اجعلها جزءاً من أسلوب كتابتك اليومي. ثانياً، تعلم كيف تجمع بين التقنيات المختلفة — مثلاً، استخدم الـ Conditional Types مع الـ Mapped Types لإنشاء أنواع ذكية تتفاعل مع السياق. ثالثاً، لا تخف من التجربة. اكتب أنواعاً معقدة، ثم حاول كسرها. كلما فهمت كيف يفشل النوع، كلما فهمت كيف يعمل. وأخيراً، اقرأ الكود المصدر لمكتبات مثل React وZod وTypeORM. هذه المكتبات مليئة بالأمثلة الحقيقية على كيفية استخدام الـ Advanced Types لحل مشاكل حقيقية.
الخطوة التالية التي أنصحك بها هي أن تأخذ مشروعك الحالي، وتحدد مكاناً واحداً فيه يمكنك تطبيق إحدى التقنيات التي تعلمتها هنا. مثلاً، إذا كان لديك نوع متكرر مثل `User` و`PartialUser`، استخدم الـ Mapped Types لإنشاء نوع واحد ديناميكي. أو إذا كان لديك دالة تتعامل مع أنواع مختلفة، استخدم الـ Conditional Types لجعلها أكثر ذكاءً. المهم هو أن تبدأ الآن، لأن كل يوم تمر دون استخدام هذه التقنيات هو يوم تضيع فيه فرصة لجعل الكود أكثر قوة ومرونة.