أتقن الأنواع المساعدة في TypeScript: أدوات قوية لتحويل الأنواع، تحسين إعادة استخدام الكود، وتعزيز أمان الأنواع في تطبيقاتك.
الأنواع المساعدة في TypeScript: أدوات مدمجة لمعالجة الأنواع
تُعد TypeScript لغة قوية تجلب الكتابة الثابتة (static typing) إلى JavaScript. إحدى ميزاتها الرئيسية هي القدرة على معالجة الأنواع، مما يسمح للمطورين بإنشاء كود أكثر قوة وقابلية للصيانة. توفر TypeScript مجموعة من الأنواع المساعدة المدمجة التي تبسط تحويلات الأنواع الشائعة. هذه الأنواع المساعدة هي أدوات لا تقدر بثمن لتعزيز أمان الأنواع، وتحسين إعادة استخدام الكود، وتبسيط سير عمل التطوير الخاص بك. يستكشف هذا الدليل الشامل أهم الأنواع المساعدة في TypeScript، مع تقديم أمثلة عملية ورؤى قابلة للتنفيذ لمساعدتك على إتقانها.
ما هي الأنواع المساعدة في TypeScript؟
الأنواع المساعدة هي عوامل تشغيل أنواع محددة مسبقًا تقوم بتحويل الأنواع الموجودة إلى أنواع جديدة. هي مدمجة في لغة TypeScript وتوفر طريقة موجزة وتصريحية لإجراء معالجات الأنواع الشائعة. يمكن أن يقلل استخدام الأنواع المساعدة بشكل كبير من الكود المتكرر ويجعل تعريفات الأنواع الخاصة بك أكثر تعبيرًا وسهولة في الفهم.
فكر فيها كدوال تعمل على الأنواع بدلاً من القيم. تأخذ نوعًا كمدخل وتعيد نوعًا معدلاً كمخرج. يتيح لك هذا إنشاء علاقات وتحويلات أنواع معقدة بأقل قدر من الكود.
لماذا نستخدم الأنواع المساعدة؟
هناك العديد من الأسباب المقنعة لدمج الأنواع المساعدة في مشاريع TypeScript الخاصة بك:
- زيادة أمان الأنواع: تساعدك الأنواع المساعدة على فرض قيود أكثر صرامة على الأنواع، مما يقلل من احتمالية حدوث أخطاء في وقت التشغيل ويحسن الموثوقية العامة للكود الخاص بك.
- تحسين إعادة استخدام الكود: باستخدام الأنواع المساعدة، يمكنك إنشاء مكونات ودوال عامة تعمل مع مجموعة متنوعة من الأنواع، مما يعزز إعادة استخدام الكود ويقلل من التكرار.
- تقليل الكود المتكرر: توفر الأنواع المساعدة طريقة موجزة وتصريحية لإجراء تحويلات الأنواع الشائعة، مما يقلل من كمية الكود المتكرر الذي تحتاج إلى كتابته.
- تعزيز قابلية القراءة: تجعل الأنواع المساعدة تعريفات الأنواع الخاصة بك أكثر تعبيرًا وسهولة في الفهم، مما يحسن من قابلية قراءة الكود الخاص بك وصيانته.
الأنواع المساعدة الأساسية في TypeScript
دعنا نستكشف بعض الأنواع المساعدة الأكثر استخدامًا وفائدة في TypeScript. سنغطي الغرض منها، وصيغتها، ونقدم أمثلة عملية لتوضيح استخدامها.
1. Partial<T>
النوع المساعد Partial<T>
يجعل جميع خصائص النوع T
اختيارية. هذا مفيد عندما تريد إنشاء نوع جديد يحتوي على بعض أو كل خصائص نوع موجود، لكنك لا تريد أن يكون وجودها جميعًا مطلوبًا.
الصيغة:
type Partial<T> = { [P in keyof T]?: T[P]; };
مثال:
interface User {
id: number;
name: string;
email: string;
}
type OptionalUser = Partial<User>; // جميع الخصائص الآن اختيارية
const partialUser: OptionalUser = {
name: "Alice", // توفير خاصية الاسم فقط
};
حالة الاستخدام: تحديث كائن بخصائص معينة فقط. على سبيل المثال، تخيل نموذج تحديث ملف تعريف مستخدم. لا تريد أن تطلب من المستخدمين تحديث كل حقل في وقت واحد.
2. Required<T>
النوع المساعد Required<T>
يجعل جميع خصائص النوع T
مطلوبة. وهو عكس Partial<T>
. هذا مفيد عندما يكون لديك نوع به خصائص اختيارية، وتريد التأكد من وجود جميع الخصائص.
الصيغة:
type Required<T> = { [P in keyof T]-?: T[P]; };
مثال:
interface Config {
apiKey?: string;
apiUrl?: string;
}
type CompleteConfig = Required<Config>; // جميع الخصائص الآن مطلوبة
const config: CompleteConfig = {
apiKey: "your-api-key",
apiUrl: "https://example.com/api",
};
حالة الاستخدام: فرض توفير جميع إعدادات التكوين قبل بدء التطبيق. يمكن أن يساعد هذا في منع أخطاء وقت التشغيل الناتجة عن الإعدادات المفقودة أو غير المعرفة.
3. Readonly<T>
النوع المساعد Readonly<T>
يجعل جميع خصائص النوع T
للقراءة فقط. هذا يمنعك من تعديل خصائص كائن عن طريق الخطأ بعد إنشائه. هذا يعزز الثبات (immutability) ويحسن من قابلية التنبؤ بالكود الخاص بك.
الصيغة:
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
مثال:
interface Product {
id: number;
name: string;
price: number;
}
type ImmutableProduct = Readonly<Product>; // جميع الخصائص الآن للقراءة فقط
const product: ImmutableProduct = {
id: 123,
name: "Example Product",
price: 25.99,
};
// product.price = 29.99; // خطأ: لا يمكن التعيين إلى 'price' لأنها خاصية للقراءة فقط.
حالة الاستخدام: إنشاء هياكل بيانات غير قابلة للتغيير، مثل كائنات التكوين أو كائنات نقل البيانات (DTOs)، التي لا ينبغي تعديلها بعد الإنشاء. هذا مفيد بشكل خاص في نماذج البرمجة الوظيفية.
4. Pick<T, K extends keyof T>
النوع المساعد Pick<T, K extends keyof T>
ينشئ نوعًا جديدًا عن طريق اختيار مجموعة من الخصائص K
من النوع T
. هذا مفيد عندما تحتاج فقط إلى مجموعة فرعية من خصائص نوع موجود.
الصيغة:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
مثال:
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
type EmployeeNameAndDepartment = Pick<Employee, "name" | "department">; // اختر فقط الاسم والقسم
const employeeInfo: EmployeeNameAndDepartment = {
name: "Bob",
department: "Engineering",
};
حالة الاستخدام: إنشاء كائنات نقل بيانات متخصصة (DTOs) تحتوي فقط على البيانات اللازمة لعملية معينة. يمكن أن يحسن هذا الأداء ويقلل من كمية البيانات المنقولة عبر الشبكة. تخيل إرسال تفاصيل المستخدم إلى العميل ولكن مع استبعاد المعلومات الحساسة مثل الراتب. يمكنك استخدام Pick لإرسال `id` و `name` فقط.
5. Omit<T, K extends keyof any>
النوع المساعد Omit<T, K extends keyof any>
ينشئ نوعًا جديدًا عن طريق حذف مجموعة من الخصائص K
من النوع T
. هذا هو عكس Pick<T, K extends keyof T>
وهو مفيد عندما تريد استبعاد خصائص معينة من نوع موجود.
الصيغة:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
مثال:
interface Event {
id: number;
title: string;
description: string;
date: Date;
location: string;
}
type EventSummary = Omit<Event, "description" | "location">; // احذف الوصف والموقع
const eventPreview: EventSummary = {
id: 1,
title: "Conference",
date: new Date(),
};
حالة الاستخدام: إنشاء إصدارات مبسطة من نماذج البيانات لأغراض محددة، مثل عرض ملخص لحدث دون تضمين الوصف الكامل والموقع. يمكن استخدام هذا أيضًا لإزالة الحقول الحساسة قبل إرسال البيانات إلى العميل.
6. Exclude<T, U>
النوع المساعد Exclude<T, U>
ينشئ نوعًا جديدًا عن طريق استبعاد جميع الأنواع القابلة للتعيين إلى U
من T
. هذا مفيد عندما تريد إزالة أنواع معينة من نوع اتحادي (union type).
الصيغة:
type Exclude<T, U> = T extends U ? never : T;
مثال:
type AllowedFileTypes = "image" | "video" | "audio" | "document";
type MediaFileTypes = "image" | "video" | "audio";
type DocumentFileTypes = Exclude<AllowedFileTypes, MediaFileTypes>; // "document"
const fileType: DocumentFileTypes = "document";
حالة الاستخدام: تصفية نوع اتحادي لإزالة أنواع محددة غير ذات صلة في سياق معين. على سبيل المثال، قد ترغب في استبعاد أنواع ملفات معينة من قائمة أنواع الملفات المسموح بها.
7. Extract<T, U>
النوع المساعد Extract<T, U>
ينشئ نوعًا جديدًا عن طريق استخلاص جميع الأنواع القابلة للتعيين إلى U
من T
. هذا هو عكس Exclude<T, U>
وهو مفيد عندما تريد تحديد أنواع معينة من نوع اتحادي.
الصيغة:
type Extract<T, U> = T extends U ? T : never;
مثال:
type InputTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = string | number | boolean;
type NonNullablePrimitives = Extract<InputTypes, PrimitiveTypes>; // string | number | boolean
const value: NonNullablePrimitives = "hello";
حالة الاستخدام: تحديد أنواع معينة من نوع اتحادي بناءً على معايير معينة. على سبيل المثال، قد ترغب في استخلاص جميع الأنواع الأولية (primitive types) من نوع اتحادي يتضمن كلاً من الأنواع الأولية وأنواع الكائنات.
8. NonNullable<T>
النوع المساعد NonNullable<T>
ينشئ نوعًا جديدًا عن طريق استبعاد null
و undefined
من النوع T
. هذا مفيد عندما تريد التأكد من أن النوع لا يمكن أن يكون null
أو undefined
.
الصيغة:
type NonNullable<T> = T extends null | undefined ? never : T;
مثال:
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
const message: DefinitelyString = "Hello, world!";
حالة الاستخدام: فرض أن القيمة ليست null
أو undefined
قبل إجراء عملية عليها. يمكن أن يساعد هذا في منع أخطاء وقت التشغيل الناتجة عن قيم null أو undefined غير المتوقعة. ضع في اعتبارك سيناريو تحتاج فيه إلى معالجة عنوان مستخدم، ومن الأهمية بمكان ألا يكون العنوان null قبل أي عملية.
9. ReturnType<T extends (...args: any) => any>
النوع المساعد ReturnType<T extends (...args: any) => any>
يستخلص نوع الإرجاع لنوع الدالة T
. هذا مفيد عندما تريد معرفة نوع القيمة التي تعيدها الدالة.
الصيغة:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
مثال:
function fetchData(url: string): Promise<{ data: any }> {
return fetch(url).then(response => response.json());
}
type FetchDataReturnType = ReturnType<typeof fetchData>; // Promise<{ data: any }>
async function processData(data: FetchDataReturnType) {
// ...
}
حالة الاستخدام: تحديد نوع القيمة التي تعيدها دالة، خاصة عند التعامل مع العمليات غير المتزامنة أو تواقيع الدوال المعقدة. يتيح لك هذا التأكد من أنك تتعامل مع القيمة المرجعة بشكل صحيح.
10. Parameters<T extends (...args: any) => any>
النوع المساعد Parameters<T extends (...args: any) => any>
يستخلص أنواع معلمات نوع الدالة T
كمجموعة مرتبة (tuple). هذا مفيد عندما تريد معرفة أنواع الوسائط التي تقبلها الدالة.
الصيغة:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
مثال:
function createUser(name: string, age: number, email: string): void {
// ...
}
type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]
function logUser(...args: CreateUserParams) {
console.log("Creating user with:", args);
}
حالة الاستخدام: تحديد أنواع الوسائط التي تقبلها دالة، وهو ما يمكن أن يكون مفيدًا لإنشاء دوال عامة أو مُزخرفات (decorators) تحتاج إلى العمل مع دوال ذات تواقيع مختلفة. يساعد على ضمان أمان الأنواع عند تمرير الوسائط إلى دالة بشكل ديناميكي.
11. ConstructorParameters<T extends abstract new (...args: any) => any>
النوع المساعد ConstructorParameters<T extends abstract new (...args: any) => any>
يستخلص أنواع معلمات نوع دالة البناء T
كمجموعة مرتبة (tuple). هذا مفيد عندما تريد معرفة أنواع الوسائط التي يقبلها المُنشئ (constructor).
الصيغة:
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
مثال:
class Logger {
constructor(public prefix: string, public enabled: boolean) {}
log(message: string) {
if (this.enabled) {
console.log(`${this.prefix}: ${message}`);
}
}
}
type LoggerConstructorParams = ConstructorParameters<typeof Logger>; // [string, boolean]
function createLogger(...args: LoggerConstructorParams) {
return new Logger(...args);
}
حالة الاستخدام: مشابه لـ Parameters
، ولكن خصيصًا لدوال البناء. يساعد عند إنشاء المصانع (factories) أو أنظمة حقن التبعية حيث تحتاج إلى إنشاء مثيلات من الفئات ديناميكيًا بتواقيع مُنشئ مختلفة.
12. InstanceType<T extends abstract new (...args: any) => any>
النوع المساعد InstanceType<T extends abstract new (...args: any) => any>
يستخلص نوع المثيل (instance type) لنوع دالة البناء T
. هذا مفيد عندما تريد معرفة نوع الكائن الذي ينشئه المُنشئ.
الصيغة:
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
مثال:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
type GreeterInstance = InstanceType<typeof Greeter>; // Greeter
const myGreeter: GreeterInstance = new Greeter("World");
console.log(myGreeter.greet());
حالة الاستخدام: تحديد نوع الكائن الذي تم إنشاؤه بواسطة مُنشئ، وهو أمر مفيد عند العمل مع الوراثة أو تعدد الأشكال (polymorphism). يوفر طريقة آمنة من حيث النوع للإشارة إلى مثيل فئة.
13. Record<K extends keyof any, T>
النوع المساعد Record<K extends keyof any, T>
ينشئ نوع كائن تكون مفاتيح خصائصه هي K
وقيم خصائصه هي T
. هذا مفيد لإنشاء أنواع تشبه القاموس حيث تعرف المفاتيح مسبقًا.
الصيغة:
type Record<K extends keyof any, T> = { [P in K]: T; };
مثال:
type CountryCode = "US" | "CA" | "GB" | "DE";
type CurrencyMap = Record<CountryCode, string>; // { US: string; CA: string; GB: string; DE: string; }
const currencies: CurrencyMap = {
US: "USD",
CA: "CAD",
GB: "GBP",
DE: "EUR",
};
حالة الاستخدام: إنشاء كائنات تشبه القاموس حيث لديك مجموعة ثابتة من المفاتيح وتريد التأكد من أن جميع المفاتيح لها قيم من نوع معين. هذا شائع عند العمل مع ملفات التكوين، أو تعيينات البيانات، أو جداول البحث.
الأنواع المساعدة المخصصة
بينما تكون الأنواع المساعدة المدمجة في TypeScript قوية، يمكنك أيضًا إنشاء أنواع مساعدة مخصصة خاصة بك لتلبية احتياجات محددة في مشاريعك. يتيح لك هذا تغليف تحويلات الأنواع المعقدة وإعادة استخدامها في جميع أنحاء قاعدة الكود الخاصة بك.
مثال:
// نوع مساعد للحصول على مفاتيح كائن لها نوع معين
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
interface Person {
name: string;
age: number;
address: string;
phoneNumber: number;
}
type StringKeys = KeysOfType<Person, string>; // "name" | "address"
أفضل الممارسات لاستخدام الأنواع المساعدة
- استخدم أسماء وصفية: أعطِ أنواعك المساعدة أسماء ذات معنى تشير بوضوح إلى الغرض منها. هذا يحسن من قابلية قراءة الكود الخاص بك وصيانته.
- وثق أنواعك المساعدة: أضف تعليقات لشرح ما تفعله أنواعك المساعدة وكيف يجب استخدامها. يساعد هذا المطورين الآخرين على فهم الكود الخاص بك واستخدامه بشكل صحيح.
- اجعلها بسيطة: تجنب إنشاء أنواع مساعدة معقدة للغاية يصعب فهمها. قم بتقسيم التحويلات المعقدة إلى أنواع مساعدة أصغر وأكثر قابلية للإدارة.
- اختبر أنواعك المساعدة: اكتب اختبارات وحدة للتأكد من أن أنواعك المساعدة تعمل بشكل صحيح. يساعد هذا في منع الأخطاء غير المتوقعة ويضمن أن أنواعك تتصرف كما هو متوقع.
- ضع في اعتبارك الأداء: على الرغم من أن الأنواع المساعدة لا يكون لها تأثير كبير على الأداء بشكل عام، كن على دراية بمدى تعقيد تحويلات الأنواع الخاصة بك، خاصة في المشاريع الكبيرة.
الخاتمة
تُعد الأنواع المساعدة في TypeScript أدوات قوية يمكنها تحسين أمان الأنواع وإعادة الاستخدام وقابلية الصيانة للكود الخاص بك بشكل كبير. من خلال إتقان هذه الأنواع المساعدة، يمكنك كتابة تطبيقات TypeScript أكثر قوة وتعبيرًا. لقد غطى هذا الدليل أهم الأنواع المساعدة في TypeScript، مع تقديم أمثلة عملية ورؤى قابلة للتنفيذ لمساعدتك في دمجها في مشاريعك.
تذكر أن تجرب هذه الأنواع المساعدة وتستكشف كيف يمكن استخدامها لحل مشكلات محددة في الكود الخاص بك. كلما أصبحت أكثر دراية بها، ستجد نفسك تستخدمها أكثر فأكثر لإنشاء تطبيقات TypeScript أنظف وأكثر قابلية للصيانة وأكثر أمانًا من حيث النوع. سواء كنت تبني تطبيقات ويب أو تطبيقات من جانب الخادم أو أي شيء بينهما، توفر الأنواع المساعدة مجموعة قيمة من الأدوات لتحسين سير عمل التطوير الخاص بك وجودة الكود الخاص بك. من خلال الاستفادة من أدوات معالجة الأنواع المدمجة هذه، يمكنك إطلاق العنان للإمكانات الكاملة لـ TypeScript وكتابة كود معبر وقوي في نفس الوقت.