أطلق العنان لقوة أنواع TypeScript المعينة لتحويلات الكائنات الديناميكية وتعديلات الخصائص المرنة، مما يعزز إعادة استخدام التعليمات البرمجية وأمان النوع للمطورين العالميين.
أنواع TypeScript المعينة: إتقان تحويل الكائنات وتعديل الخصائص
في المشهد المتطور باستمرار لتطوير البرامج، تعد أنظمة الأنواع القوية ذات أهمية قصوى لبناء تطبيقات قابلة للصيانة وقابلة للتطوير وموثوقة. أصبحت TypeScript، بما تتمتع به من استدلال قوي للأنواع وميزات متقدمة، أداة لا غنى عنها للمطورين في جميع أنحاء العالم. من بين أقوى قدراتها الأنواع المعينة، وهي آلية متطورة تسمح لنا بتحويل أنواع الكائنات الموجودة إلى أنواع جديدة. ستتعمق مدونة المنشور هذه في عالم أنواع TypeScript المعينة، واستكشاف مفاهيمها الأساسية وتطبيقاتها العملية وكيف تمكن المطورين من التعامل بأناقة مع تحويلات الكائنات وتعديلات الخصائص.
فهم المفهوم الأساسي للأنواع المعينة
في جوهرها، النوع المعين هو طريقة لإنشاء أنواع جديدة عن طريق تكرار خصائص نوع موجود. فكر في الأمر على أنه حلقة للأنواع. بالنسبة لكل خاصية في النوع الأصلي، يمكنك تطبيق تحويل على مفتاحها أو قيمتها أو كليهما. يفتح هذا مجموعة واسعة من الاحتمالات لإنشاء تعريفات أنواع جديدة بناءً على الأنواع الموجودة، دون تكرار يدوي.
يتضمن بناء الجملة الأساسي للنوع المعين بنية { [P in K]: T }، حيث:
P: يمثل اسم الخاصية التي يتم تكرارها.in K: هذا هو الجزء الحاسم، مما يشير إلى أنPستأخذ كل مفتاح من النوعK(والذي عادةً ما يكون اتحادًا للمسلسلات النصية الحرفية، أو نوع keyof).T: يحدد نوع القيمة للخاصيةPفي النوع الجديد.
لنبدأ بمثال بسيط. تخيل أن لديك كائنًا يمثل بيانات المستخدم، وتريد إنشاء نوع جديد تكون فيه جميع الخصائص اختيارية. هذا سيناريو شائع، على سبيل المثال، عند إنشاء كائنات التكوين أو عند تنفيذ تحديثات جزئية.
مثال 1: جعل جميع الخصائص اختيارية
ضع في اعتبارك هذا النوع الأساسي:
type User = {
id: number;
name: string;
email: string;
isActive: boolean;
};
يمكننا إنشاء نوع جديد، OptionalUser، حيث تكون جميع هذه الخصائص اختيارية باستخدام نوع معين:
type OptionalUser = {
[P in keyof User]?: User[P];
};
دعونا نحلل هذا:
keyof User: يقوم هذا بإنشاء اتحاد لمفاتيح النوعUser(على سبيل المثال،'id' | 'name' | 'email' | 'isActive').P in keyof User: يتكرر هذا على كل مفتاح في الاتحاد.?: هذا هو المعدِّل الذي يجعل الخاصية اختيارية.User[P]: هذا نوع بحث. لكل مفتاحP، فإنه يسترجع نوع القيمة المقابل من النوعUserالأصلي.
سيبدو نوع OptionalUser الناتج كالتالي:
{
id?: number;
name?: string;
email?: string;
isActive?: boolean;
}
هذا قوي بشكل لا يصدق. بدلاً من إعادة تعريف كل خاصية يدويًا بعلامة ?، قمنا بإنشاء النوع ديناميكيًا. يمكن توسيع هذا المبدأ لإنشاء العديد من أنواع الأدوات المساعدة الأخرى.
معدلات الخصائص الشائعة في الأنواع المعينة
لا تقتصر الأنواع المعينة على جعل الخصائص اختيارية فحسب. إنها تتيح لك تطبيق معدلات مختلفة على خصائص النوع الناتج. تتضمن الأكثر شيوعًا ما يلي:
- الاختيارية: إضافة أو إزالة المعدِّل
?. - للقراءة فقط: إضافة أو إزالة المعدِّل
readonly. - القابلية للقيم الخالية/عدم القابلية للقيم الخالية: إضافة أو إزالة
| nullأو| undefined.
مثال 2: إنشاء إصدار للقراءة فقط من النوع
على غرار جعل الخصائص اختيارية، يمكننا إنشاء نوع ReadonlyUser:
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
سينتج عن هذا:
{
readonly id: number;
readonly name: string;
readonly email: string;
readonly isActive: boolean;
}
هذا مفيد للغاية للتأكد من أن بعض هياكل البيانات، بمجرد إنشائها، لا يمكن تغييرها، وهو مبدأ أساسي لبناء أنظمة قوية وقابلة للتنبؤ، خاصة في البيئات المتزامنة أو عند التعامل مع أنماط البيانات غير القابلة للتغيير الشائعة في نماذج البرمجة الوظيفية التي تتبناها العديد من فرق التطوير الدولية.
مثال 3: الجمع بين الاختيارية والقراءة فقط
يمكننا الجمع بين المعدلات. على سبيل المثال، نوع تكون فيه الخصائص اختيارية للقراءة فقط:
type OptionalReadonlyUser = {
readonly [P in keyof User]?: User[P];
};
وينتج عن هذا:
{
readonly id?: number;
readonly name?: string;
readonly email?: string;
readonly isActive?: boolean;
}
إزالة المعدِّلات باستخدام الأنواع المعينة
ماذا لو كنت تريد إزالة مُعدِّل؟ تسمح TypeScript بذلك باستخدام بناء الجملة -? و-readonly داخل الأنواع المعينة. هذا قوي بشكل خاص عند التعامل مع أنواع الأدوات المساعدة الموجودة أو تركيبات الأنواع المعقدة.
لنفترض أن لديك نوع Partial<T> (وهو مضمن ويجعل جميع الخصائص اختيارية)، وتريد إنشاء نوع مطابق لـ Partial<T> ولكن مع جعل جميع الخصائص إلزامية مرة أخرى.
type Mandatory<T> = {
-?: T extends object ? T[keyof T] : never;
};
type FullyPopulatedUser = Mandatory<Partial<User>>;
يبدو هذا غير بديهي. دعونا نحلل الأمر:
Partial<User> مكافئ لـ OptionalUser. الآن، نريد أن نجعل خصائصه إلزامية. يزيل بناء الجملة -? المعدِّل الاختياري.
تتمثل الطريقة الأكثر مباشرة لتحقيق ذلك، دون الاعتماد على Partial أولاً، في أخذ النوع الأصلي ببساطة وجعله إلزاميًا إذا كان اختياريًا:
type MakeMandatory<T> = {
-?: T;
};
type MandatoryUser = MakeMandatory<OptionalUser>;
سيؤدي هذا إلى إرجاع OptionalUser بشكل صحيح إلى بنية النوع User الأصلية (جميع الخصائص موجودة ومطلوبة).
وبالمثل، لإزالة المعدِّل readonly:
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type MutableUser = Mutable<ReadonlyUser>;
سيكون MutableUser مكافئًا للنوع User الأصلي، ولكن خصائصه لن تكون للقراءة فقط.
القابلية للقيم الخالية وعدم التحديد
يمكنك أيضًا التحكم في القابلية للقيم الخالية. على سبيل المثال، للتأكد من أن جميع الخصائص ليست قابلة للقيم الخالية بالتأكيد:
type NonNullableValues<T> = {
[P in keyof T]-?: NonNullable<T[P]>;
};
interface MaybeNull {
name: string | null;
age: number | undefined;
}
type DefiniteValues = NonNullableValues<MaybeNull>;
// type DefiniteValues = {
// name: string;
// age: number;
// }
هنا، تضمن -? أن الخصائص ليست اختيارية، وتزيل NonNullable<T[P]> null وundefined من نوع القيمة.
تحويل مفاتيح الخصائص
الأنواع المعينة متعددة الاستخدامات بشكل لا يصدق، ولا تتوقف عند تعديل القيم أو المعدلات فحسب. يمكنك أيضًا تحويل مفاتيح نوع الكائن. هذا هو المكان الذي تتألق فيه الأنواع المعينة حقًا في السيناريوهات المعقدة.
مثال 4: إضافة بادئة إلى مفاتيح الخصائص
لنفترض أنك تريد إنشاء نوع جديد حيث تحتوي جميع خصائص النوع الموجود على بادئة معينة. يمكن أن يكون هذا مفيدًا لتعيين مساحة الاسم أو لإنشاء اختلافات في هياكل البيانات.
type Prefixed<T, Prefix extends string> = {
[P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};
type OriginalConfig = {
timeout: number;
retries: number;
};
type PrefixedConfig = Prefixed<OriginalConfig, 'app'>;
// type PrefixedConfig = {
// appTimeout: number;
// appRetries: number;
// }
دعونا نحلل تحويل المفتاح:
P in keyof T: لا يزال يتكرر على المفاتيح الأصلية.as `${Prefix}${Capitalize<string & P>}`: هذا هو بند إعادة تعيين المفتاح.`${Prefix}${...}`: يستخدم هذا أنواع المسلسلات النصية الحرفية لإنشاء اسم المفتاح الجديد عن طريق ربطPrefixالمقدم باسم الخاصية المحول.Capitalize<string & P>: هذا نمط شائع لضمان التعامل مع اسم الخاصيةPكسلسلة، ثم يتم تحويله إلى أحرف كبيرة. نستخدمstring & PلتقاطعPمعstring، مما يضمن أن تتعامل TypeScript معه على أنه نوع سلسلة، وهو ضروري لـCapitalize.
يوضح هذا المثال كيف يمكنك إعادة تسمية الخصائص ديناميكيًا بناءً على الخصائص الموجودة، وهي تقنية قوية للحفاظ على الاتساق عبر طبقات مختلفة من التطبيق أو عند التكامل مع الأنظمة الخارجية التي لها اصطلاحات تسمية محددة.
مثال 5: تصفية الخصائص
ماذا لو كنت تريد فقط تضمين الخصائص التي تفي بشرط معين؟ يمكن تحقيق ذلك من خلال الجمع بين الأنواع المعينة والأنواع الشرطية وبند as لإعادة تعيين المفتاح، غالبًا لتصفية الخصائص.
type OnlyStrings<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
interface MixedData {
name: string;
age: number;
city: string;
isActive: boolean;
}
type StringOnlyData = OnlyStrings<MixedData>;
// type StringOnlyData = {
// name: string;
// city: string;
// }
في هذه الحالة:
T[P] extends string ? P : never: بالنسبة لكل خاصيةP، نتحقق مما إذا كان نوع القيمة الخاص بها (T[P]) قابلاً للتخصيص لـstring.- إذا كانت سلسلة، فسيتم الاحتفاظ بالمفتاح
P. - إذا لم تكن سلسلة، فسيتم تعيينها إلى
never. عند تعيين مفتاح إلىnever، تتم إزالته فعليًا من نوع الكائن الناتج.
هذه التقنية لا تقدر بثمن لإنشاء أنواع أكثر تحديدًا من أنواع أوسع، على سبيل المثال، استخراج إعدادات التكوين فقط التي هي من نوع معين، أو فصل حقول البيانات حسب طبيعتها.
مثال 6: تحويل المفاتيح إلى شكل مختلف
يمكنك أيضًا تحويل المفاتيح إلى أنواع مختلفة تمامًا من المفاتيح، على سبيل المثال، تحويل مفاتيح السلسلة إلى أرقام، أو العكس، على الرغم من أن هذا أقل شيوعًا لمعالجة الكائنات المباشرة وأكثر للبرمجة المتقدمة على مستوى النوع.
ضع في اعتبارك تحويل مفاتيح السلسلة إلى اتحاد للمسلسلات النصية الحرفية، ثم استخدام ذلك كأساس لنوع جديد. على الرغم من عدم تحويل مفاتيح الكائن مباشرةً *داخل* النوع المعين نفسه بهذه الطريقة المحددة، إلا أنه يوضح كيف يمكن معالجة المفاتيح.
يمكن أن يكون مثال تحويل المفتاح الأكثر مباشرة هو تعيين المفاتيح إلى إصداراتها الكبيرة:
type UppercaseKeys<T> = {
[P in keyof T as Uppercase<string & P>]: T[P];
};
type LowercaseData = {
firstName: string;
lastName: string;
};
type UppercaseData = UppercaseKeys<LowercaseData>;
// type UppercaseData = {
// FIRSTNAME: string;
// LASTNAME: string;
// }
يستخدم هذا بند as لتحويل كل مفتاح P إلى مكافئه بالأحرف الكبيرة.
التطبيقات العملية والسيناريوهات الواقعية
الأنواع المعينة ليست مجرد تركيبات نظرية؛ لها آثار عملية كبيرة عبر مجالات تطوير مختلفة. فيما يلي بعض السيناريوهات الشائعة التي لا تقدر بثمن فيها:
1. بناء أنواع الأدوات المساعدة القابلة لإعادة الاستخدام
يمكن تضمين العديد من تحويلات الأنواع الشائعة في أنواع أدوات مساعدة قابلة لإعادة الاستخدام. توفر مكتبة TypeScript القياسية بالفعل أمثلة ممتازة مثل Partial<T> وReadonly<T> وRecord<K, T> وPick<T, K>. يمكنك تحديد أنواع الأدوات المساعدة المخصصة الخاصة بك باستخدام الأنواع المعينة لتبسيط سير عمل التطوير الخاص بك.
على سبيل المثال، نوع يقوم بتعيين جميع الخصائص إلى وظائف تقبل القيمة الأصلية وإرجاع قيمة جديدة:
type Mappers<T> = {
[P in keyof T]: (value: T[P]) => T[P];
};
interface ProductInfo {
name: string;
price: number;
}
type ProductMappers = Mappers<ProductInfo>;
// type ProductMappers = {
// name: (value: string) => string;
// price: (value: number) => number;
// }
2. معالجة النماذج الديناميكية والتحقق من الصحة
في تطوير الواجهة الأمامية، خاصة مع أطر العمل مثل React أو Angular (على الرغم من أن الأمثلة هنا هي TypeScript خالصة)، تعد معالجة النماذج وحالات التحقق من الصحة الخاصة بها مهمة شائعة. يمكن أن تساعد الأنواع المعينة في إدارة حالة التحقق من الصحة لكل حقل من حقول النموذج.
ضع في اعتبارك نموذجًا به حقول يمكن أن تكون "أصلية" أو "ملموسة" أو "صحيحة" أو "غير صالحة".
type FormFieldState = 'pristine' | 'touched' | 'dirty' | 'valid' | 'invalid';
type FormState<T> = {
[P in keyof T]: FormFieldState;
};
interface UserForm {
username: string;
email: string;
password: string;
}
type UserFormState = FormState<UserForm>;
// type UserFormState = {
// username: FormFieldState;
// email: FormFieldState;
// password: FormFieldState;
// }
يتيح لك ذلك إنشاء نوع يعكس بنية بيانات النموذج الخاص بك ولكنه بدلاً من ذلك يتتبع حالة كل حقل، مما يضمن الاتساق وأمان النوع لمنطق إدارة النموذج الخاص بك. هذا مفيد بشكل خاص للمشاريع الدولية حيث قد تؤدي متطلبات واجهة المستخدم/تجربة المستخدم المتنوعة إلى حالات نموذج معقدة.
3. تحويل استجابة واجهة برمجة التطبيقات
عند التعامل مع واجهات برمجة التطبيقات، قد لا تتطابق بيانات الاستجابة دائمًا تمامًا مع نماذج المجال الداخلية الخاصة بك. يمكن أن تساعد الأنواع المعينة في تحويل استجابات واجهة برمجة التطبيقات إلى الشكل المطلوب.
تخيل استجابة واجهة برمجة تطبيقات تستخدم snake_case للمفاتيح، لكن تطبيقك يفضل camelCase:
// Assume this is the incoming API response type
type ApiUserData = {
user_id: number;
first_name: string;
last_name: string;
};
// Helper to convert snake_case to camelCase for keys
type ToCamelCase<S extends string>: string = S extends `${infer T}_${infer U}`
? `${T}${Capitalize<U>}`
: S;
type CamelCasedKeys<T> = {
[P in keyof T as ToCamelCase<string & P>]: T[P];
};
type AppUserData = CamelCasedKeys<ApiUserData>;
// type AppUserData = {
// userId: number;
// firstName: string;
// lastName: string;
// }
هذا مثال أكثر تقدمًا باستخدام نوع شرطي متكرر لمعالجة السلسلة. الخلاصة الرئيسية هي أن الأنواع المعينة، عند دمجها مع ميزات TypeScript المتقدمة الأخرى، يمكن أن تؤتمت تحويلات البيانات المعقدة، مما يوفر وقت التطوير ويقلل من خطر حدوث أخطاء في وقت التشغيل. هذا أمر بالغ الأهمية للفرق العالمية التي تعمل مع خدمات خلفية متنوعة.
4. تحسين هياكل تشبه التعداد
بينما تحتوي TypeScript على enums، قد تحتاج أحيانًا إلى مزيد من المرونة أو اشتقاق أنواع من حرفيات الكائن التي تعمل مثل التعدادات.
const AppPermissions = {
READ: 'read',
WRITE: 'write',
DELETE: 'delete',
ADMIN: 'admin',
} as const;
type Permission = typeof AppPermissions[keyof typeof AppPermissions];
// type Permission = 'read' | 'write' | 'delete' | 'admin'
type UserPermissions = {
[P in Permission]?: boolean;
};
type RolePermissions = {
[P in Permission]: boolean;
};
const userPerms: UserPermissions = {
read: true,
};
const adminRole: RolePermissions = {
read: true,
write: true,
delete: true,
admin: true,
};
هنا، نشتق أولاً نوع اتحاد لجميع سلاسل الأذونات الممكنة. بعد ذلك، نستخدم الأنواع المعينة لإنشاء أنواع حيث يكون كل إذن مفتاحًا، مما يسمح لنا بتحديد ما إذا كان لدى المستخدم هذا الإذن (اختياري) أو إذا كان الدور يفرضه (مطلوب). هذا النمط شائع في أنظمة التفويض في جميع أنحاء العالم.
التحديات والاعتبارات
في حين أن الأنواع المعينة قوية بشكل لا يصدق، فمن المهم أن تكون على دراية بالتعقيدات المحتملة:
- قابلية القراءة والتعقيد: يمكن أن تصبح الأنواع المعينة المعقدة للغاية صعبة القراءة والفهم، خاصة بالنسبة للمطورين الجدد في هذه الميزات المتقدمة. اسع دائمًا إلى الوضوح وفكر في إضافة تعليقات أو تقسيم التحويلات المعقدة.
- آثار الأداء: في حين أن التحقق من نوع TypeScript يتم في وقت الترجمة، إلا أن عمليات معالجة الأنواع المعقدة للغاية يمكن أن تزيد قليلاً من أوقات الترجمة من الناحية النظرية. بالنسبة لمعظم التطبيقات، هذا لا يكاد يذكر، ولكنه نقطة يجب وضعها في الاعتبار بالنسبة لقواعد التعليمات البرمجية الكبيرة جدًا أو عمليات البناء بالغة الأهمية للأداء.
- تصحيح الأخطاء: عندما ينتج النوع المعين نتيجة غير متوقعة، قد يكون تصحيح الأخطاء أمرًا صعبًا في بعض الأحيان. يعد استخدام ميزات فحص النوع في TypeScript Playground أو IDE أمرًا بالغ الأهمية لفهم كيفية حل الأنواع.
- فهم
keyofوأنواع البحث: يعتمد الاستخدام الفعال للأنواع المعينة على فهم قوي لـkeyofوأنواع البحث (T[P]). تأكد من أن فريقك لديه فهم جيد لهذه المفاهيم التأسيسية.
أفضل الممارسات لاستخدام الأنواع المعينة
للاستفادة من الإمكانات الكاملة للأنواع المعينة مع التخفيف من تحدياتها، ضع في اعتبارك أفضل الممارسات التالية:
- ابدأ ببساطة: ابدأ بالاختيارية الأساسية وتحويلات القراءة فقط قبل الغوص في عمليات إعادة تعيين المفاتيح المعقدة أو المنطق الشرطي.
- الاستفادة من أنواع الأدوات المساعدة المضمنة: تعرف على أنواع الأدوات المساعدة المضمنة في TypeScript مثل
PartialوReadonlyوRecordوPickوOmitوExclude. غالبًا ما تكون كافية للمهام الشائعة ويتم اختبارها وفهمها جيدًا. - إنشاء أنواع عامة قابلة لإعادة الاستخدام: قم بتغليف أنماط النوع المعين الشائعة في أنواع الأدوات المساعدة العامة. يعزز هذا الاتساق ويقلل من التعليمات البرمجية القياسية عبر مشروعك وللفرق العالمية.
- استخدم أسماء وصفية: قم بتسمية الأنواع المعينة والمعلمات العامة الخاصة بك بوضوح للإشارة إلى الغرض منها (على سبيل المثال،
Optional<T>،DeepReadonly<T>،PrefixedKeys<T, Prefix>). - إعطاء الأولوية لقابلية القراءة: إذا أصبح النوع المعين معقدًا للغاية، ففكر فيما إذا كانت هناك طريقة أبسط لتحقيق نفس النتيجة أو ما إذا كان الأمر يستحق التعقيد الإضافي. في بعض الأحيان، يكون تعريف النوع الأكثر تفصيلاً ولكنه أكثر وضوحًا مفضلاً.
- توثيق الأنواع المعقدة: بالنسبة للأنواع المعينة المعقدة، أضف تعليقات JSDoc تشرح وظائفها، خاصة عند مشاركة التعليمات البرمجية داخل فريق دولي متنوع.
- اختبر أنواعك: اكتب اختبارات النوع أو استخدم أمثلة للتحقق من أن الأنواع المعينة الخاصة بك تتصرف على النحو المتوقع. هذا مهم بشكل خاص للتحويلات المعقدة حيث قد يكون من الصعب اكتشاف الأخطاء الطفيفة.
الخلاصة
تعد أنواع TypeScript المعينة حجر الزاوية في معالجة الأنواع المتقدمة، مما يوفر للمطورين قوة لا مثيل لها لتحويل أنواع الكائنات وتكييفها. سواء كنت تجعل الخصائص اختيارية أو للقراءة فقط أو إعادة تسميتها أو تصفيتها بناءً على شروط معقدة، فإن الأنواع المعينة توفر طريقة تعريفية وآمنة من الناحية النوعية ومعبرة للغاية لإدارة هياكل البيانات الخاصة بك.
من خلال إتقان هذه التقنيات، يمكنك تحسين إعادة استخدام التعليمات البرمجية بشكل كبير، وتحسين أمان النوع، وبناء تطبيقات أكثر قوة وقابلية للصيانة. احتضن قوة الأنواع المعينة لرفع مستوى تطوير TypeScript الخاص بك والمساهمة في بناء حلول برمجية عالية الجودة لجمهور عالمي. أثناء تعاونك مع مطورين من مناطق مختلفة، يمكن أن تكون أنماط الأنواع المتقدمة هذه بمثابة لغة مشتركة لضمان جودة التعليمات البرمجية واتساقها، وسد فجوات الاتصال المحتملة من خلال صرامة نظام الأنواع.