با تایپهای دقیق TypeScript برای تطبیق سختگیرانه شکل آبجکت، جلوگیری از ویژگیهای غیرمنتظره و تضمین استحکام کد آشنا شوید. کاربردهای عملی و بهترین شیوهها را بیاموزید.
تایپهای دقیق در TypeScript: تطبیق سختگیرانه شکل آبجکت برای کدی استوار
تایپاسکریپت (TypeScript)، یک فرا مجموعه از جاوااسکریپت، تایپبندی استاتیک را به دنیای پویای توسعه وب میآورد. در حالی که تایپاسکریپت مزایای قابل توجهی از نظر ایمنی نوع (type safety) و قابلیت نگهداری کد ارائه میدهد، سیستم تایپبندی ساختاری آن گاهی اوقات میتواند منجر به رفتار غیرمنتظره شود. اینجاست که مفهوم «تایپهای دقیق» (exact types) به میان میآید. اگرچه تایپاسکریپت ویژگی داخلی با نام صریح «تایپهای دقیق» ندارد، اما میتوانیم با ترکیبی از ویژگیها و تکنیکهای تایپاسکریپت به رفتار مشابهی دست یابیم. این پست وبلاگ به بررسی چگونگی اعمال تطبیق سختگیرانهتر شکل آبجکت در تایپاسکریپت برای بهبود استحکام کد و جلوگیری از خطاهای رایج میپردازد.
درک تایپبندی ساختاری در 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
در این سناریو، تایپاسکریپت اجازه میدهد `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` دارد. تایپاسکریپت خطایی ایجاد نمیکند زیرا `myConfig` همچنان اینترفیس `Config` را برآورده میکند. با این حال، این اشتباه تایپی هرگز شناسایی نمیشود و اگر منظور از `typo` مثلاً `typoo` بوده باشد، ممکن است برنامه آنطور که انتظار میرود رفتار نکند. این مسائل به ظاهر بیاهمیت میتوانند هنگام دیباگ کردن برنامههای پیچیده به مشکلات بزرگی تبدیل شوند. یک ویژگی گمشده یا با املای اشتباه میتواند به ویژه هنگام کار با آبجکتهای تودرتو، سخت شناسایی شود.
رویکردهایی برای اعمال تایپهای دقیق در TypeScript
در حالی که «تایپهای دقیق» واقعی به طور مستقیم در تایپاسکریپت در دسترس نیستند، در اینجا چندین تکنیک برای دستیابی به نتایج مشابه و اعمال تطبیق سختگیرانهتر شکل آبجکت ارائه شده است:
۱. استفاده از Type Assertions با `Omit`
تایپ کمکی `Omit` به شما امکان میدهد با حذف ویژگیهای خاصی از یک تایپ موجود، یک تایپ جدید ایجاد کنید. این ویژگی در ترکیب با یک type assertion میتواند به جلوگیری از ویژگیهای اضافی کمک کند.
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
۲. استفاده از یک تابع برای ایجاد آبجکتها
شما میتوانید یک تابع سازنده (factory function) ایجاد کنید که فقط ویژگیهای تعریفشده در اینترفیس را بپذیرد. این رویکرد بررسی نوع قوی را در نقطه ایجاد آبجکت فراهم میکند.
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` ساخته شده است، تضمین میکنید که هیچ ویژگی اضافی نمیتواند وارد شود. این کار ایجاد پیکربندی را امنتر میکند.
۳. استفاده از Type Guards
Type guardها توابعی هستند که نوع یک متغیر را در یک محدوده خاص محدودتر میکنند. در حالی که آنها مستقیماً از ویژگیهای اضافی جلوگیری نمیکنند، میتوانند به شما کمک کنند تا به صراحت آنها را بررسی کرده و اقدام مناسب را انجام دهید.
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");
}
در این مثال، type guard به نام `isUser` نه تنها وجود ویژگیهای مورد نیاز را بررسی میکند، بلکه انواع آنها و تعداد *دقیق* ویژگیها را نیز کنترل میکند. این رویکرد صریحتر است و به شما امکان میدهد با آبجکتهای نامعتبر به شیوهای مناسب برخورد کنید. با این حال، بررسی تعداد ویژگیها شکننده است. هر زمان که `User` ویژگیهایی را به دست آورد یا از دست بدهد، این بررسی باید بهروز شود.
۴. بهرهگیری از `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'.
این کار تغییرناپذیری (immutability) را بهبود میبخشد، اما فقط از جهش (mutation) جلوگیری میکند، نه وجود ویژگیهای اضافی را. در ترکیب با `Omit` یا رویکرد تابع، مؤثرتر میشود.
۵. استفاده از کتابخانهها (مانند 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 آورده شده است:
- انتخاب تکنیک مناسب: بهترین رویکرد به نیازهای خاص و الزامات پروژه شما بستگی دارد. برای موارد ساده، type assertions با `Omit` یا توابع سازنده ممکن است کافی باشد. برای سناریوهای پیچیدهتر یا زمانی که اعتبارسنجی زمان اجرا مورد نیاز است، استفاده از کتابخانههایی مانند Zod یا io-ts را در نظر بگیرید.
- سازگار باشید: رویکرد انتخابی خود را به طور مداوم در سراسر کدبیس خود اعمال کنید تا سطح یکنواختی از ایمنی نوع را حفظ کنید.
- تایپهای خود را مستند کنید: اینترفیسها و تایپهای خود را به وضوح مستند کنید تا شکل مورد انتظار دادههای خود را به سایر توسعهدهندگان منتقل کنید.
- کد خود را تست کنید: تستهای واحد بنویسید تا تأیید کنید که محدودیتهای نوع شما همانطور که انتظار میرود کار میکنند و کد شما دادههای نامعتبر را به درستی مدیریت میکند.
- معایب و مزایا را در نظر بگیرید: اعمال تطبیق سختگیرانهتر شکل آبجکت میتواند کد شما را قویتر کند، اما همچنین میتواند زمان توسعه را افزایش دهد. مزایا را در برابر هزینهها بسنجید و رویکردی را انتخاب کنید که برای پروژه شما منطقیتر است.
- پذیرش تدریجی: اگر روی یک کدبیس بزرگ موجود کار میکنید، این تکنیکها را به تدریج اتخاذ کنید و از حیاتیترین بخشهای برنامه خود شروع کنید.
- هنگام تعریف شکل آبجکتها، اینترفیسها را به تایپهای مستعار ترجیح دهید: اینترفیسها به طور کلی ترجیح داده میشوند زیرا از ادغام تعاریف (declaration merging) پشتیبانی میکنند، که میتواند برای گسترش تایپها در فایلهای مختلف مفید باشد.
مثالهای دنیای واقعی
بیایید به چند سناریوی واقعی نگاه کنیم که در آنها تایپهای دقیق میتوانند مفید باشند:
- پیلودهای درخواست API: هنگام ارسال داده به یک API، اطمینان از اینکه پیلود با اسکیمای مورد انتظار مطابقت دارد، حیاتی است. اعمال تایپهای دقیق میتواند از خطاهای ناشی از ارسال ویژگیهای غیرمنتظره جلوگیری کند. برای مثال، بسیاری از APIهای پردازش پرداخت به دادههای غیرمنتظره بسیار حساس هستند.
- فایلهای پیکربندی: فایلهای پیکربندی اغلب شامل تعداد زیادی ویژگی هستند و اشتباهات تایپی میتوانند رایج باشند. استفاده از تایپهای دقیق میتواند به شناسایی این اشتباهات در مراحل اولیه کمک کند. اگر در حال تنظیم مکانهای سرور در یک استقرار ابری هستید، یک اشتباه تایپی در تنظیم مکان (مثلاً 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`) ممکن است نادیده گرفته شود و منجر به عدم نمایش ترجمهها در رابط کاربری شود. با اعمال تطبیق سختگیرانهتر شکل آبجکت، میتوانید این خطاها را در حین توسعه شناسایی کرده و از رسیدن آنها به محیط تولید جلوگیری کنید.
نتیجهگیری
در حالی که تایپاسکریپت «تایپهای دقیق» داخلی ندارد، میتوانید با استفاده از ترکیبی از ویژگیهای تایپاسکریپت و تکنیکهایی مانند type assertions با `Omit`، توابع سازنده، type guardها، `Readonly`، `as const` و کتابخانههای خارجی مانند Zod و io-ts به نتایج مشابهی دست یابید. با اعمال تطبیق سختگیرانهتر شکل آبجکت، میتوانید استحکام کد خود را بهبود بخشید، از خطاهای رایج جلوگیری کنید و برنامههای خود را قابل اعتمادتر سازید. به یاد داشته باشید که رویکردی را انتخاب کنید که به بهترین وجه با نیازهای شما مطابقت دارد و در اعمال آن در سراسر کدبیس خود سازگار باشید. با در نظر گرفتن دقیق این رویکردها، میتوانید کنترل بیشتری بر تایپهای برنامه خود داشته باشید و قابلیت نگهداری بلندمدت را افزایش دهید.