استكشف أنواع TypeScript الدقيقة لمطابقة شكل الكائن الصارمة، ومنع الخصائص غير المتوقعة وضمان قوة التعليمات البرمجية. تعلم التطبيقات العملية وأفضل الممارسات.
أنواع TypeScript الدقيقة: مطابقة شكل الكائن الصارمة لتعليمات برمجية قوية
TypeScript، وهي مجموعة فائقة من JavaScript، تجلب الكتابة الثابتة إلى عالم تطوير الويب الديناميكي. في حين أن TypeScript يوفر مزايا كبيرة من حيث سلامة النوع وقابلية صيانة التعليمات البرمجية، فإن نظام الكتابة الهيكلية الخاص به يمكن أن يؤدي في بعض الأحيان إلى سلوك غير متوقع. هذا هو المكان الذي يأتي فيه مفهوم "الأنواع الدقيقة". على الرغم من أن TypeScript لا يحتوي على ميزة مضمنة تسمى صراحة "الأنواع الدقيقة"، يمكننا تحقيق سلوك مماثل من خلال مجموعة من ميزات وتقنيات TypeScript. ستتعمق مشاركة المدونة هذه في كيفية فرض مطابقة شكل الكائن الأكثر صرامة في TypeScript لتحسين قوة التعليمات البرمجية ومنع الأخطاء الشائعة.
فهم الكتابة الهيكلية لـ TypeScript
تستخدم TypeScript الكتابة الهيكلية (المعروفة أيضًا باسم كتابة البط) ، مما يعني أن توافق النوع يتم تحديده من خلال أعضاء الأنواع، بدلاً من أسمائهم المعلنة. إذا كان للكائن جميع الخصائص المطلوبة بواسطة نوع ما، فإنه يعتبر متوافقًا مع هذا النوع، بغض النظر عما إذا كان لديه خصائص إضافية.
على سبيل المثال:
interface Point {
x: number;
y: number;
}
const myPoint = { x: 10, y: 20, z: 30 };
function printPoint(point: Point) {
console.log(`X: ${point.x}, Y: ${point.y}`);
}
printPoint(myPoint); // This works fine, even though myPoint has the 'z' property
في هذا السيناريو، تسمح TypeScript بمرور `myPoint` إلى `printPoint` لأنه يحتوي على خصائص `x` و `y` المطلوبة، على الرغم من أنه يحتوي على خاصية `z` إضافية. في حين أن هذه المرونة يمكن أن تكون مريحة، إلا أنها يمكن أن تؤدي أيضًا إلى أخطاء خفية إذا مررت عن غير قصد كائنات ذات خصائص غير متوقعة.
المشكلة في الخصائص الزائدة
قد تؤدي تساهل الكتابة الهيكلية في بعض الأحيان إلى إخفاء الأخطاء. ضع في اعتبارك دالة تتوقع كائن تكوين:
interface Config {
apiUrl: string;
timeout: number;
}
function setup(config: Config) {
console.log(`API URL: ${config.apiUrl}`);
console.log(`Timeout: ${config.timeout}`);
}
const myConfig = { apiUrl: "https://api.example.com", timeout: 5000, typo: true };
setup(myConfig); // TypeScript doesn't complain here!
console.log(myConfig.typo); //prints true. The extra property silently exists
في هذا المثال، يحتوي `myConfig` على خاصية إضافية `typo`. لا تثير TypeScript خطأً لأن `myConfig` لا يزال يفي بواجهة `Config`. ومع ذلك، لم يتم اكتشاف الخطأ الإملائي على الإطلاق، وقد لا يتصرف التطبيق كما هو متوقع إذا كان من المفترض أن يكون الخطأ الإملائي `typoo`. يمكن أن تنمو هذه المشكلات التي تبدو غير ذات أهمية إلى صداع كبير عند تصحيح التطبيقات المعقدة. قد يكون من الصعب بشكل خاص اكتشاف خاصية مفقودة أو بها خطأ إملائي عند التعامل مع كائنات متداخلة داخل كائنات.
أساليب فرض الأنواع الدقيقة في TypeScript
على الرغم من أن "الأنواع الدقيقة" الحقيقية غير متوفرة بشكل مباشر في TypeScript، إليك العديد من التقنيات لتحقيق نتائج مماثلة وفرض مطابقة شكل كائن أكثر صرامة:
1. استخدام تأكيدات النوع مع `Omit`
يتيح لك نوع الأداة المساعدة `Omit` إنشاء نوع جديد عن طريق استبعاد خصائص معينة من نوع موجود. جنبًا إلى جنب مع تأكيد النوع، يمكن أن يساعد هذا في منع الخصائص الزائدة.
interface Point {
x: number;
y: number;
}
const myPoint = { x: 10, y: 20, z: 30 };
// Create a type that includes only the properties of Point
const exactPoint: Point = myPoint as Omit & Point;
// Error: Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'.
// Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
function printPoint(point: Point) {
console.log(`X: ${point.x}, Y: ${point.y}`);
}
//Fix
const myPointCorrect = { x: 10, y: 20 };
const exactPointCorrect: Point = myPointCorrect as Omit & Point;
printPoint(exactPointCorrect);
يطرح هذا النهج خطأ إذا كان لدى `myPoint` خصائص غير معرفة في واجهة `Point`.
التفسير: ينشئ `Omit
2. استخدام دالة لإنشاء كائنات
يمكنك إنشاء دالة مصنع تقبل فقط الخصائص المحددة في الواجهة. يوفر هذا النهج فحصًا قويًا للنوع عند نقطة إنشاء الكائن.
interface Config {
apiUrl: string;
timeout: number;
}
function createConfig(config: Config): Config {
return {
apiUrl: config.apiUrl,
timeout: config.timeout,
};
}
const myConfig = createConfig({ apiUrl: "https://api.example.com", timeout: 5000 });
//This will not compile:
//const myConfigError = createConfig({ apiUrl: "https://api.example.com", timeout: 5000, typo: true });
//Argument of type '{ apiUrl: string; timeout: number; typo: true; }' is not assignable to parameter of type 'Config'.
// Object literal may only specify known properties, and 'typo' does not exist in type 'Config'.
عن طريق إرجاع كائن تم إنشاؤه باستخدام الخصائص المحددة فقط في واجهة `Config`، فإنك تضمن أنه لا يمكن التسلل إلى أي خصائص إضافية. هذا يجعله أكثر أمانًا لإنشاء التكوين.
3. استخدام حراس النوع
حراس النوع عبارة عن دوال تضيق نوع متغير ضمن نطاق معين. على الرغم من أنها لا تمنع الخصائص الزائدة بشكل مباشر، إلا أنها يمكن أن تساعدك في التحقق منها بشكل صريح واتخاذ الإجراءات المناسبة.
interface User {
id: number;
name: string;
}
function isUser(obj: any): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj && typeof obj.id === 'number' &&
'name' in obj && typeof obj.name === 'string' &&
Object.keys(obj).length === 2 //check for number of keys. Note: brittle and depends on User's exact key count.
);
}
const potentialUser1 = { id: 123, name: "Alice" };
const potentialUser2 = { id: 456, name: "Bob", extra: true };
if (isUser(potentialUser1)) {
console.log("Valid User:", potentialUser1.name);
} else {
console.log("Invalid User");
}
if (isUser(potentialUser2)) {
console.log("Valid User:", potentialUser2.name); //Will not hit here
} else {
console.log("Invalid User");
}
في هذا المثال، يتحقق حارس النوع `isUser` ليس فقط من وجود الخصائص المطلوبة ولكن أيضًا من أنواعها وعدد الخصائص *الدقيقة*. هذا النهج أكثر صراحة ويتيح لك التعامل مع الكائنات غير الصالحة بأمان. ومع ذلك، فإن فحص عدد الخصائص هش. كلما اكتسبت `User` / فقدت الخصائص، يجب تحديث الفحص.
4. الاستفادة من `Readonly` و `as const`
بينما تمنع `Readonly` تعديل الخصائص الموجودة، و `as const` ينشئ صفًا أو كائنًا للقراءة فقط حيث تكون جميع الخصائص للقراءة فقط بعمق ولها أنواع حرفية، يمكن استخدامها لإنشاء تعريف أكثر صرامة وفحص النوع عند دمجهما مع طرق أخرى. على الرغم من ذلك، لا يمنع أي منهما الخصائص الزائدة بمفرده.
interface Options {
width: number;
height: number;
}
//Create the Readonly type
type ReadonlyOptions = Readonly;
const options: ReadonlyOptions = { width: 100, height: 200 };
//options.width = 300; //error: Cannot assign to 'width' because it is a read-only property.
//Using as const
const config = { api_url: "https://example.com", timeout: 3000 } as const;
//config.timeout = 5000; //error: Cannot assign to 'timeout' because it is a read-only property.
//However, excess properties are still allowed:
const invalidOptions: ReadonlyOptions = { width: 100, height: 200, depth: 300 }; //no error. Still allows excess properties.
interface StrictOptions {
readonly width: number;
readonly height: number;
}
//This will now error:
//const invalidStrictOptions: StrictOptions = { width: 100, height: 200, depth: 300 };
//Type '{ width: number; height: number; depth: number; }' is not assignable to type 'StrictOptions'.
// Object literal may only specify known properties, and 'depth' does not exist in type 'StrictOptions'.
هذا يحسن عدم القابلية للتغيير، ولكنه يمنع فقط الطفرة، وليس وجود خصائص إضافية. جنبًا إلى جنب مع `Omit`، أو أسلوب الوظيفة، يصبح أكثر فعالية.
5. استخدام المكتبات (مثل Zod و io-ts)
توفر مكتبات مثل Zod و io-ts إمكانات قوية للتحقق من نوع وقت التشغيل وتعريف المخطط. تتيح لك هذه المكتبات تحديد مخططات تصف بدقة الشكل المتوقع لبياناتك، بما في ذلك منع الخصائص الزائدة. في حين أنها تضيف تبعية وقت التشغيل، إلا أنها توفر حلاً قويًا ومرنًا للغاية.
مثال مع Zod:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer;
const validUser = { id: 1, name: "John" };
const invalidUser = { id: 2, name: "Jane", extra: true };
const parsedValidUser = UserSchema.parse(validUser);
console.log("Parsed Valid User:", parsedValidUser);
try {
const parsedInvalidUser = UserSchema.parse(invalidUser);
console.log("Parsed Invalid User:", parsedInvalidUser); // This won't be reached
} catch (error) {
console.error("Validation Error:", error.errors);
}
ستطرح طريقة `parse` في Zod خطأً إذا لم تتوافق الإدخال مع المخطط، مما يمنع بشكل فعال الخصائص الزائدة. يوفر هذا التحقق من صحة وقت التشغيل ويقوم أيضًا بإنشاء أنواع TypeScript من المخطط، مما يضمن الاتساق بين تعريفات الأنواع ومنطق التحقق من صحة وقت التشغيل.
أفضل الممارسات لفرض الأنواع الدقيقة
فيما يلي بعض أفضل الممارسات التي يجب مراعاتها عند فرض مطابقة شكل كائن أكثر صرامة في TypeScript:
- اختر التقنية الصحيحة: يعتمد أفضل نهج على احتياجاتك ومتطلبات مشروعك المحددة. بالنسبة للحالات البسيطة، قد تكون تأكيدات النوع مع `Omit` أو دوال المصنع كافية. بالنسبة للسيناريوهات الأكثر تعقيدًا أو عند الحاجة إلى التحقق من صحة وقت التشغيل، ضع في اعتبارك استخدام مكتبات مثل Zod أو io-ts.
- كن متسقًا: قم بتطبيق النهج الذي اخترته باستمرار في جميع أنحاء قاعدة التعليمات البرمجية الخاصة بك للحفاظ على مستوى موحد من سلامة النوع.
- وثق أنواعك: قم بتوثيق واجهاتك وأنواعك بوضوح لتوصيل الشكل المتوقع لبياناتك إلى المطورين الآخرين.
- اختبر الكود الخاص بك: اكتب اختبارات وحدة للتحقق من أن قيود النوع الخاصة بك تعمل على النحو المتوقع وأن التعليمات البرمجية الخاصة بك تتعامل مع البيانات غير الصالحة بأمان.
- ضع في اعتبارك المقايضات: يمكن أن يؤدي فرض مطابقة شكل كائن أكثر صرامة إلى جعل التعليمات البرمجية الخاصة بك أكثر قوة، ولكنه قد يزيد أيضًا من وقت التطوير. قم بموازنة الفوائد مقابل التكاليف واختر النهج الذي له أكبر قدر من المعنى لمشروعك.
- الاعتماد التدريجي: إذا كنت تعمل على قاعدة تعليمات برمجية موجودة كبيرة، ففكر في اعتماد هذه التقنيات تدريجيًا، بدءًا من الأجزاء الأكثر أهمية في تطبيقك.
- تفضيل الواجهات على أسماء أنواع عند تعريف أشكال الكائنات: تفضل الواجهات بشكل عام لأنها تدعم دمج الإعلانات، والذي يمكن أن يكون مفيدًا لتوسيع الأنواع عبر ملفات مختلفة.
أمثلة من العالم الحقيقي
دعنا نلقي نظرة على بعض السيناريوهات الواقعية حيث يمكن أن تكون الأنواع الدقيقة مفيدة:
- حمولات طلبات واجهة برمجة التطبيقات: عند إرسال بيانات إلى واجهة برمجة التطبيقات، من الضروري التأكد من أن الحمولة تتوافق مع المخطط المتوقع. يمكن أن يمنع فرض الأنواع الدقيقة الأخطاء الناتجة عن إرسال خصائص غير متوقعة. على سبيل المثال، تكون العديد من واجهات برمجة تطبيقات معالجة الدفع حساسة للغاية للبيانات غير المتوقعة.
- ملفات التكوين: غالبًا ما تحتوي ملفات التكوين على عدد كبير من الخصائص، ويمكن أن تكون الأخطاء الإملائية شائعة. يمكن أن يساعد استخدام الأنواع الدقيقة في اكتشاف هذه الأخطاء الإملائية في وقت مبكر. إذا كنت تقوم بإعداد مواقع الخادم في عملية نشر السحابة، فسيكون من الصعب للغاية تصحيح خطأ إملائي في إعداد الموقع (على سبيل المثال eu-west-1 مقابل eu-wet-1) إذا لم يتم اكتشافه في المقدمة.
- خطوط أنابيب تحويل البيانات: عند تحويل البيانات من تنسيق إلى آخر، من المهم التأكد من أن بيانات الإخراج تتوافق مع المخطط المتوقع.
- قوائم انتظار الرسائل: عند إرسال رسائل عبر قائمة انتظار الرسائل، من المهم التأكد من أن حمولة الرسالة صالحة وتحتوي على الخصائص الصحيحة.
مثال: تكوين التدويل (i18n)
تخيل إدارة الترجمات لتطبيق متعدد اللغات. قد يكون لديك كائن تكوين مثل هذا:
interface Translation {
greeting: string;
farewell: string;
}
interface I18nConfig {
locale: string;
translations: Translation;
}
const englishConfig: I18nConfig = {
locale: "en-US",
translations: {
greeting: "Hello",
farewell: "Goodbye"
}
};
//This will be an issue, as an excess property exists, silently introducing a bug.
const spanishConfig: I18nConfig = {
locale: "es-ES",
translations: {
greeting: "Hola",
farewell: "Adiós",
typo: "unintentional translation"
}
};
//Solution: Using Omit
const spanishConfigCorrect: I18nConfig = {
locale: "es-ES",
translations: {
greeting: "Hola",
farewell: "Adiós"
} as Omit & Translation
};
بدون أنواع دقيقة، يمكن أن يمر خطأ إملائي في مفتاح ترجمة (مثل إضافة حقل `typo`) دون أن يلاحظه أحد، مما يؤدي إلى فقدان الترجمات في واجهة المستخدم. من خلال فرض مطابقة شكل كائن أكثر صرامة، يمكنك اكتشاف هذه الأخطاء أثناء التطوير ومنعها من الوصول إلى الإنتاج.
الخلاصة
في حين أن TypeScript لا يحتوي على "أنواع دقيقة" مضمنة، يمكنك تحقيق نتائج مماثلة باستخدام مجموعة من ميزات وتقنيات TypeScript مثل تأكيدات النوع مع `Omit`، ودوال المصنع، وحراس النوع، `Readonly`، `as const`، والمكتبات الخارجية مثل Zod و io-ts. من خلال فرض مطابقة شكل كائن أكثر صرامة، يمكنك تحسين قوة التعليمات البرمجية الخاصة بك، ومنع الأخطاء الشائعة، وجعل تطبيقاتك أكثر موثوقية. تذكر أن تختار النهج الذي يناسب احتياجاتك على أفضل وجه وأن تكون متسقًا في تطبيقه في جميع أنحاء قاعدة التعليمات البرمجية الخاصة بك. من خلال النظر بعناية في هذه الأساليب، يمكنك التحكم بشكل أكبر في أنواع تطبيقك وزيادة إمكانية الصيانة على المدى الطويل.