أطلق العنان لقوة TypeScript مع الأنواع الشرطية والمُحوَّلة المتقدمة. تعلم كيفية إنشاء تطبيقات مرنة وآمنة من حيث النوع تتكيف مع هياكل البيانات المعقدة. أتقن فن كتابة كود TypeScript ديناميكي حقًا.
أنماط TypeScript المتقدمة: إتقان الأنواع الشرطية والمُحوَّلة
تكمن قوة TypeScript في قدرتها على توفير أنواع قوية (strong typing)، مما يسمح لك باكتشاف الأخطاء مبكرًا وكتابة كود أكثر قابلية للصيانة. في حين أن الأنواع الأساسية مثل string
، number
، و boolean
تعد أساسية، فإن ميزات TypeScript المتقدمة مثل الأنواع الشرطية والمُحوَّلة تفتح بُعدًا جديدًا من المرونة وأمان الأنواع. سيتعمق هذا الدليل الشامل في هذه المفاهيم القوية، ويزودك بالمعرفة اللازمة لإنشاء تطبيقات TypeScript ديناميكية وقابلة للتكيف حقًا.
ما هي الأنواع الشرطية؟
تسمح لك الأنواع الشرطية بتعريف أنواع تعتمد على شرط، على غرار المعامل الثلاثي في JavaScript (condition ? trueValue : falseValue
). إنها تمكنك من التعبير عن علاقات أنواع معقدة بناءً على ما إذا كان نوع ما يلبي قيدًا معينًا.
الصيغة (Syntax)
الصيغة الأساسية للنوع الشرطي هي:
T extends U ? X : Y
T
: النوع الذي يتم التحقق منه.U
: النوع الذي يتم التحقق مقابله.extends
: الكلمة الأساسية التي تشير إلى علاقة نوع فرعي.X
: النوع الذي سيتم استخدامه إذا كانT
قابلاً للإسناد إلىU
.Y
: النوع الذي سيتم استخدامه إذا لم يكنT
قابلاً للإسناد إلىU
.
في جوهر الأمر، إذا تم تقييم T extends U
إلى true، فسيتم حل النوع إلى X
؛ وإلا، فسيتم حله إلى Y
.
أمثلة عملية
1. تحديد نوع معامل الدالة
لنفترض أنك تريد إنشاء نوع يحدد ما إذا كان معامل الدالة هو سلسلة نصية (string) أو رقم (number):
type ParamType<T> = T extends string ? string : number;
function processValue(value: ParamType<string | number>): void {
if (typeof value === "string") {
console.log("Value is a string:", value);
} else {
console.log("Value is a number:", value);
}
}
processValue("hello"); // Output: Value is a string: hello
processValue(123); // Output: Value is a number: 123
في هذا المثال، ParamType<T>
هو نوع شرطي. إذا كان T
سلسلة نصية، فسيتم حل النوع إلى string
؛ وإلا، فسيتم حله إلى number
. تقبل الدالة processValue
إما سلسلة نصية أو رقمًا بناءً على هذا النوع الشرطي.
2. استخلاص نوع الإرجاع بناءً على نوع الإدخال
تخيل سيناريو لديك فيه دالة تُرجع أنواعًا مختلفة بناءً على المدخلات. يمكن أن تساعدك الأنواع الشرطية في تحديد نوع الإرجاع الصحيح:
interface StringProcessor {
process(input: string): number;
}
interface NumberProcessor {
process(input: number): string;
}
type Processor<T> = T extends string ? StringProcessor : NumberProcessor;
function createProcessor<T extends string | number>(input: T): Processor<T> {
if (typeof input === "string") {
return { process: (input: string) => input.length } as Processor<T>;
} else {
return { process: (input: number) => input.toString() } as Processor<T>;
}
}
const stringProcessor = createProcessor("example");
const numberProcessor = createProcessor(42);
console.log(stringProcessor.process("example")); // Output: 7
console.log(numberProcessor.process(42)); // Output: "42"
هنا، يختار النوع Processor<T>
بشكل شرطي إما StringProcessor
أو NumberProcessor
بناءً على نوع الإدخال. هذا يضمن أن الدالة createProcessor
تُرجع كائن المعالج من النوع الصحيح.
3. الاتحادات المُميَّزة (Discriminated Unions)
تعتبر الأنواع الشرطية قوية للغاية عند التعامل مع الاتحادات المُميَّزة. الاتحاد المُميَّز هو نوع اتحاد حيث يكون لكل عضو خاصية نوع مشتركة وفردية (المُميِّز). يسمح لك هذا بتضييق النوع بناءً على قيمة تلك الخاصية.
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Circle;
type Area<T extends Shape> = T extends { kind: "square" } ? number : string;
function calculateArea(shape: Shape): Area<typeof shape> {
if (shape.kind === "square") {
return shape.size * shape.size;
} else {
return Math.PI * shape.radius * shape.radius;
}
}
const mySquare: Square = { kind: "square", size: 5 };
const myCircle: Circle = { kind: "circle", radius: 3 };
console.log(calculateArea(mySquare)); // Output: 25
console.log(calculateArea(myCircle)); // Output: 28.274333882308138
في هذا المثال، النوع Shape
هو اتحاد مُميَّز. يستخدم النوع Area<T>
نوعًا شرطيًا لتحديد ما إذا كان الشكل مربعًا أم دائرة، مع إرجاع number
للمربعات وstring
للدوائر (على الرغم من أنه في سيناريو واقعي، من المحتمل أن ترغب في أنواع إرجاع متسقة، إلا أن هذا يوضح المبدأ).
نقاط رئيسية حول الأنواع الشرطية
- تمكين تعريف الأنواع بناءً على الشروط.
- تحسين أمان الأنواع من خلال التعبير عن علاقات أنواع معقدة.
- مفيدة للعمل مع معاملات الدوال، وأنواع الإرجاع، والاتحادات المُميَّزة.
ما هي الأنواع المُحوَّلة؟
توفر الأنواع المُحوَّلة طريقة لتحويل الأنواع الموجودة عن طريق المرور على خصائصها. تسمح لك بإنشاء أنواع جديدة بناءً على خصائص نوع آخر، مع تطبيق تعديلات مثل جعل الخصائص اختيارية، أو للقراءة فقط، أو تغيير أنواعها.
الصيغة (Syntax)
الصيغة العامة للنوع المُحوَّل هي:
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
: نوع الإدخال.keyof T
: معامل نوع يُرجع اتحادًا لجميع مفاتيح الخصائص فيT
.K in keyof T
: يكرر كل مفتاح فيkeyof T
، مع تعيين كل مفتاح لمتغير النوعK
.ModifiedType
: النوع الذي سيتم تعيين كل خاصية إليه. يمكن أن يشمل ذلك الأنواع الشرطية أو تحويلات الأنواع الأخرى.
أمثلة عملية
1. جعل الخصائص اختيارية
يمكنك استخدام نوع مُحوَّل لجعل جميع خصائص نوع موجود اختيارية:
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
const partialUser: PartialUser = {
name: "John Doe",
}; // Valid, as 'id' and 'email' are optional
هنا، PartialUser
هو نوع مُحوَّل يكرر مفاتيح الواجهة User
. لكل مفتاح K
، فإنه يجعل الخاصية اختيارية عن طريق إضافة المُعدِّل ?
. يسترد User[K]
نوع الخاصية K
من الواجهة User
.
2. جعل الخصائص للقراءة فقط (Readonly)
بالمثل، يمكنك جعل جميع خصائص نوع موجود للقراءة فقط:
interface Product {
id: number;
name: string;
price: number;
}
type ReadonlyProduct = {
readonly [K in keyof Product]: Product[K];
};
const readonlyProduct: ReadonlyProduct = {
id: 123,
name: "Example Product",
price: 25.00,
};
// readonlyProduct.price = 30.00; // Error: Cannot assign to 'price' because it is a read-only property.
في هذه الحالة، ReadonlyProduct
هو نوع مُحوَّل يضيف المُعدِّل readonly
إلى كل خاصية من خصائص الواجهة Product
.
3. تحويل أنواع الخصائص
يمكن أيضًا استخدام الأنواع المُحوَّلة لتحويل أنواع الخصائص. على سبيل المثال، يمكنك إنشاء نوع يحول جميع الخصائص النصية (string) إلى أرقام (numbers):
interface Config {
apiUrl: string;
timeout: string;
maxRetries: number;
}
type NumericConfig = {
[K in keyof Config]: Config[K] extends string ? number : Config[K];
};
const numericConfig: NumericConfig = {
apiUrl: 123, // Must be a number due to the mapping
timeout: 456, // Must be a number due to the mapping
maxRetries: 3,
};
يوضح هذا المثال استخدام نوع شرطي داخل نوع مُحوَّل. لكل خاصية K
، يتحقق مما إذا كان نوع Config[K]
هو سلسلة نصية. إذا كان كذلك، يتم تحويل النوع إلى number
؛ وإلا، فإنه يظل دون تغيير.
4. إعادة تعيين المفاتيح (منذ TypeScript 4.1)
قدم TypeScript 4.1 القدرة على إعادة تعيين المفاتيح داخل الأنواع المُحوَّلة باستخدام الكلمة الأساسية as
. يتيح لك ذلك إنشاء أنواع جديدة بأسماء خصائص مختلفة بناءً على النوع الأصلي.
interface Event {
eventId: string;
eventName: string;
eventDate: Date;
}
type TransformedEvent = {
[K in keyof Event as `new${Capitalize<string&K>}`]: Event[K];
};
// Result:
// {
// newEventId: string;
// newEventName: string;
// newEventDate: Date;
// }
//Capitalize function used to Capitalize first letter
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//Usage with an actual object
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "New Name",
newEventDate: new Date()
};
هنا، يقوم النوع TransformedEvent
بإعادة تعيين كل مفتاح K
إلى مفتاح جديد يبدأ بـ "new" ويتم تحويل أول حرف فيه إلى كبير. تضمن الدالة المساعدة `Capitalize` أن الحرف الأول من المفتاح كبير. يضمن التقاطع `string & K` أننا نتعامل فقط مع مفاتيح نصية وأننا نحصل على النوع الحرفي الصحيح من K.
تفتح إعادة تعيين المفاتيح إمكانيات قوية لتحويل وتكييف الأنواع مع احتياجات محددة. يتيح لك ذلك إعادة تسمية المفاتيح أو تصفيتها أو تعديلها بناءً على منطق معقد.
نقاط رئيسية حول الأنواع المُحوَّلة
- تمكين تحويل الأنواع الموجودة عن طريق المرور على خصائصها.
- السماح بجعل الخصائص اختيارية، أو للقراءة فقط، أو تغيير أنواعها.
- مفيدة لإنشاء أنواع جديدة بناءً على خصائص نوع آخر.
- إعادة تعيين المفاتيح (التي تم تقديمها في TypeScript 4.1) توفر مرونة أكبر في تحويلات الأنواع.
الجمع بين الأنواع الشرطية والمُحوَّلة
القوة الحقيقية للأنواع الشرطية والمُحوَّلة تظهر عند دمجها معًا. يتيح لك ذلك إنشاء تعريفات أنواع مرنة ومعبرة للغاية يمكنها التكيف مع مجموعة واسعة من السيناريوهات.
مثال: تصفية الخصائص حسب النوع
لنفترض أنك تريد إنشاء نوع يقوم بتصفية خصائص كائن بناءً على نوعها. على سبيل المثال، قد ترغب في استخلاص الخصائص النصية فقط من كائن ما.
interface Data {
name: string;
age: number;
city: string;
country: string;
isEmployed: boolean;
}
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringData = StringProperties<Data>;
// Result:
// {
// name: string;
// city: string;
// country: string;
// }
const stringData: StringData = {
name: "John",
city: "New York",
country: "USA",
};
في هذا المثال، يستخدم النوع StringProperties<T>
نوعًا مُحوَّلًا مع إعادة تعيين المفاتيح ونوع شرطي. لكل خاصية K
، يتحقق مما إذا كان نوع T[K]
هو سلسلة نصية. إذا كان كذلك، يتم الاحتفاظ بالمفتاح؛ وإلا، يتم تعيينه إلى never
، مما يؤدي إلى تصفيته بشكل فعال. يؤدي استخدام never
كمفتاح لنوع مُحوَّل إلى إزالته من النوع الناتج. هذا يضمن تضمين الخصائص النصية فقط في النوع StringData
.
الأنواع المساعدة (Utility Types) في TypeScript
يوفر TypeScript العديد من الأنواع المساعدة المدمجة التي تستفيد من الأنواع الشرطية والمُحوَّلة لإجراء تحويلات الأنواع الشائعة. يمكن أن يؤدي فهم هذه الأنواع المساعدة إلى تبسيط الكود الخاص بك بشكل كبير وتحسين أمان الأنواع.
الأنواع المساعدة الشائعة
Partial<T>
: يجعل جميع خصائصT
اختيارية.Readonly<T>
: يجعل جميع خصائصT
للقراءة فقط.Required<T>
: يجعل جميع خصائصT
مطلوبة. (يزيل المُعدِّل?
)Pick<T, K extends keyof T>
: يختار مجموعة من الخصائصK
منT
.Omit<T, K extends keyof T>
: يزيل مجموعة من الخصائصK
منT
.Record<K extends keyof any, T>
: يبني نوعًا بمجموعة من الخصائصK
من النوعT
.Exclude<T, U>
: يستبعد منT
جميع الأنواع القابلة للإسناد إلىU
.Extract<T, U>
: يستخلص منT
جميع الأنواع القابلة للإسناد إلىU
.NonNullable<T>
: يستبعدnull
وundefined
منT
.Parameters<T>
: يحصل على معاملات نوع الدالةT
في شكل tuple.ReturnType<T>
: يحصل على نوع الإرجاع لنوع الدالةT
.InstanceType<T>
: يحصل على نوع المثيل (instance) لنوع دالة مُنشِئةT
.ThisType<T>
: يعمل كعلامة لنوعthis
السياقي.
تم بناء هذه الأنواع المساعدة باستخدام الأنواع الشرطية والمُحوَّلة، مما يوضح قوة ومرونة ميزات TypeScript المتقدمة هذه. على سبيل المثال، يتم تعريف Partial<T>
على النحو التالي:
type Partial<T> = {
[P in keyof T]?: T[P];
};
أفضل الممارسات لاستخدام الأنواع الشرطية والمُحوَّلة
على الرغم من أن الأنواع الشرطية والمُحوَّلة قوية، إلا أنها يمكن أن تجعل الكود الخاص بك أكثر تعقيدًا إذا لم يتم استخدامها بعناية. إليك بعض أفضل الممارسات التي يجب وضعها في الاعتبار:
- اجعلها بسيطة: تجنب الأنواع الشرطية والمُحوَّلة المعقدة بشكل مفرط. إذا أصبح تعريف النوع معقدًا للغاية، ففكر في تقسيمه إلى أجزاء أصغر وأكثر قابلية للإدارة.
- استخدم أسماء ذات معنى: أطلق على أنواعك الشرطية والمُحوَّلة أسماء وصفية تشير بوضوح إلى الغرض منها.
- وثّق أنواعك: أضف تعليقات لشرح المنطق وراء أنواعك الشرطية والمُحوَّلة، خاصة إذا كانت معقدة.
- استفد من الأنواع المساعدة: قبل إنشاء نوع شرطي أو مُحوَّل مخصص، تحقق مما إذا كان يمكن لنوع مساعد مدمج تحقيق نفس النتيجة.
- اختبر أنواعك: تأكد من أن أنواعك الشرطية والمُحوَّلة تتصرف كما هو متوقع عن طريق كتابة اختبارات وحدة تغطي سيناريوهات مختلفة.
- ضع الأداء في الاعتبار: يمكن أن تؤثر حسابات الأنواع المعقدة على أوقات التحويل البرمجي (compile times). كن على دراية بالآثار المترتبة على الأداء لتعريفات الأنواع الخاصة بك.
الخاتمة
تعد الأنواع الشرطية والمُحوَّلة أدوات أساسية لإتقان TypeScript. إنها تمكنك من إنشاء تطبيقات مرنة للغاية وآمنة من حيث النوع وقابلة للصيانة تتكيف مع هياكل البيانات المعقدة والمتطلبات الديناميكية. من خلال فهم وتطبيق المفاهيم التي تمت مناقشتها في هذا الدليل، يمكنك إطلاق العنان للإمكانات الكاملة لـ TypeScript وكتابة كود أكثر قوة وقابلية للتطوير. بينما تستمر في استكشاف TypeScript، تذكر أن تجرب مجموعات مختلفة من الأنواع الشرطية والمُحوَّلة لاكتشاف طرق جديدة لحل مشاكل الأنواع الصعبة. الاحتمالات لا حصر لها حقًا.