فارسی

با تایپ‌های برند شده در تایپ‌اسکریپت، یک تکنیک قدرتمند برای دستیابی به تایپینگ اسمی در یک سیستم تایپ ساختاری، آشنا شوید. یاد بگیرید چگونه ایمنی تایپ و وضوح کد را افزایش دهید.

تایپ‌های برند شده در تایپ‌اسکریپت: تایپینگ اسمی در یک سیستم ساختاری

سیستم تایپ ساختاری تایپ‌اسکریپت انعطاف‌پذیری بالایی ارائه می‌دهد اما گاهی می‌تواند منجر به رفتارهای غیرمنتظره شود. تایپ‌های برند شده (Branded Types) راهی برای اعمال تایپینگ اسمی (Nominal Typing) فراهم می‌کنند که باعث افزایش ایمنی تایپ و وضوح کد می‌شود. این مقاله تایپ‌های برند شده را به تفصیل بررسی کرده و مثال‌های عملی و بهترین شیوه‌ها برای پیاده‌سازی آن‌ها را ارائه می‌دهد.

درک تفاوت تایپینگ ساختاری و اسمی

قبل از پرداختن به تایپ‌های برند شده، بیایید تفاوت بین تایپینگ ساختاری و اسمی را روشن کنیم.

تایپینگ ساختاری (Duck Typing)

در یک سیستم تایپ ساختاری، دو تایپ در صورتی سازگار در نظر گرفته می‌شوند که ساختار یکسانی داشته باشند (یعنی دارای خصوصیات یکسان با تایپ‌های یکسان باشند). تایپ‌اسکریپت از تایپینگ ساختاری استفاده می‌کند. این مثال را در نظر بگیرید:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // در تایپ‌اسکریپت معتبر است

console.log(vector.x); // خروجی: 10

با وجود اینکه Point و Vector به عنوان تایپ‌های مجزا تعریف شده‌اند، تایپ‌اسکریپت اجازه می‌دهد یک شیء Point به یک متغیر Vector اختصاص داده شود، زیرا ساختار یکسانی دارند. این می‌تواند راحت باشد، اما اگر نیاز به تمایز بین تایپ‌هایی داشته باشید که از نظر منطقی متفاوت اما از نظر شکل یکسان هستند، می‌تواند منجر به خطا شود. به عنوان مثال، مختصات طول و عرض جغرافیایی را در نظر بگیرید که ممکن است به طور اتفاقی با مختصات پیکسل‌های صفحه نمایش مطابقت داشته باشند.

تایپینگ اسمی

در یک سیستم تایپ اسمی، تایپ‌ها تنها در صورتی سازگار در نظر گرفته می‌شوند که نام یکسانی داشته باشند. حتی اگر دو تایپ ساختار یکسانی داشته باشند، در صورتی که نام‌های متفاوتی داشته باشند، به عنوان تایپ‌های مجزا در نظر گرفته می‌شوند. زبان‌هایی مانند جاوا و C# از تایپینگ اسمی استفاده می‌کنند.

نیاز به تایپ‌های برند شده

تایپینگ ساختاری تایپ‌اسکریپت زمانی مشکل‌ساز می‌شود که نیاز دارید اطمینان حاصل کنید یک مقدار به یک تایپ خاص تعلق دارد، صرف‌نظر از ساختار آن. به عنوان مثال، نمایش ارزها را در نظر بگیرید. ممکن است تایپ‌های مختلفی برای USD و EUR داشته باشید، اما هر دوی آن‌ها می‌توانند به صورت عدد نمایش داده شوند. بدون مکانیزمی برای تمایز آن‌ها، ممکن است به طور تصادفی عملیاتی را روی ارز اشتباه انجام دهید.

تایپ‌های برند شده این مشکل را با ایجاد تایپ‌های متمایزی که از نظر ساختاری مشابه اما توسط سیستم تایپ متفاوت تلقی می‌شوند، حل می‌کنند. این کار ایمنی تایپ را افزایش داده و از خطاهایی که ممکن است در غیر این صورت نادیده گرفته شوند، جلوگیری می‌کند.

پیاده‌سازی تایپ‌های برند شده در تایپ‌اسکریپت

تایپ‌های برند شده با استفاده از تایپ‌های تقاطعی (intersection types) و یک symbol منحصر به فرد یا رشته لیترال (string literal) پیاده‌سازی می‌شوند. ایده اصلی این است که یک «برند» به یک تایپ اضافه کنیم تا آن را از سایر تایپ‌ها با ساختار مشابه متمایز کند.

استفاده از Symbol (توصیه شده)

استفاده از symbol برای برندینگ عموماً ترجیح داده می‌شود زیرا symbolها تضمین شده‌اند که منحصر به فرد باشند.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// از کامنت درآوردن خط بعدی باعث خطای تایپ می‌شود
// const invalidOperation = addUSD(usd1, eur1);

در این مثال، USD و EUR تایپ‌های برند شده‌ای هستند که بر اساس تایپ number ساخته شده‌اند. unique symbol تضمین می‌کند که این تایپ‌ها متمایز هستند. توابع createUSD و createEUR برای ایجاد مقادیر این تایپ‌ها استفاده می‌شوند و تابع addUSD فقط مقادیر USD را می‌پذیرد. تلاش برای جمع کردن یک مقدار EUR با یک مقدار USD منجر به خطای تایپ خواهد شد.

استفاده از رشته‌های لیترال (String Literals)

شما می‌توانید از رشته‌های لیترال نیز برای برندینگ استفاده کنید، اگرچه این رویکرد نسبت به استفاده از symbolها کمتر قوی است زیرا رشته‌های لیترال تضمینی برای منحصر به فرد بودن ندارند.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// از کامنت درآوردن خط بعدی باعث خطای تایپ می‌شود
// const invalidOperation = addUSD(usd1, eur1);

این مثال نتیجه‌ای مشابه مثال قبلی را به دست می‌آورد، اما با استفاده از رشته‌های لیترال به جای symbolها. با اینکه ساده‌تر است، مهم است که اطمینان حاصل کنید رشته‌های لیترالی که برای برندینگ استفاده می‌شوند در کل کدبیس شما منحصر به فرد هستند.

مثال‌های عملی و موارد استفاده

تایپ‌های برند شده می‌توانند در سناریوهای مختلفی که نیاز به اعمال ایمنی تایپ فراتر از سازگاری ساختاری دارید، به کار روند.

شناسه‌ها (IDs)

سیستمی را با انواع مختلف شناسه‌ها مانند UserID، ProductID و OrderID در نظر بگیرید. همه این شناسه‌ها ممکن است به صورت عدد یا رشته نمایش داده شوند، اما شما می‌خواهید از ترکیب تصادفی انواع مختلف شناسه‌ها جلوگیری کنید.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... دریافت اطلاعات کاربر
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... دریافت اطلاعات محصول
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("User:", user);
console.log("Product:", product);

// از کامنت درآوردن خط بعدی باعث خطای تایپ می‌شود
// const invalidCall = getUser(productID);

این مثال نشان می‌دهد که چگونه تایپ‌های برند شده می‌توانند از پاس دادن یک ProductID به تابعی که انتظار یک UserID را دارد، جلوگیری کرده و ایمنی تایپ را افزایش دهند.

مقادیر مختص دامنه (Domain-Specific)

تایپ‌های برند شده همچنین می‌توانند برای نمایش مقادیر مختص دامنه با محدودیت‌های خاص مفید باشند. به عنوان مثال، ممکن است یک تایپ برای درصد داشته باشید که باید همیشه بین ۰ تا ۱۰۰ باشد.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Percentage must be between 0 and 100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Discounted Price:", discountedPrice);

  // از کامنت درآوردن خط بعدی باعث خطا در زمان اجرا می‌شود
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

این مثال نشان می‌دهد چگونه می‌توان یک محدودیت را بر روی مقدار یک تایپ برند شده در زمان اجرا اعمال کرد. در حالی که سیستم تایپ نمی‌تواند تضمین کند که مقدار Percentage همیشه بین ۰ تا ۱۰۰ است، تابع createPercentage می‌تواند این محدودیت را در زمان اجرا اعمال کند. شما همچنین می‌توانید از کتابخانه‌هایی مانند io-ts برای اعمال اعتبارسنجی زمان اجرا برای تایپ‌های برند شده استفاده کنید.

نمایش تاریخ و زمان

کار با تاریخ و زمان به دلیل فرمت‌ها و مناطق زمانی مختلف می‌تواند چالش‌برانگیز باشد. تایپ‌های برند شده می‌توانند به تمایز بین نمایش‌های مختلف تاریخ و زمان کمک کنند.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // اعتبارسنجی اینکه رشته تاریخ در فرمت UTC باشد (مثلاً ISO 8601 با Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Invalid UTC date format');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // اعتبارسنجی اینکه رشته تاریخ در فرمت محلی باشد (مثلاً YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Invalid local date format');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // انجام تبدیل منطقه زمانی
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

این مثال بین تاریخ‌های UTC و محلی تمایز قائل می‌شود و تضمین می‌کند که شما با نمایش صحیح تاریخ و زمان در بخش‌های مختلف برنامه خود کار می‌کنید. اعتبارسنجی در زمان اجرا تضمین می‌کند که فقط رشته‌های تاریخ با فرمت صحیح می‌توانند به این تایپ‌ها اختصاص داده شوند.

بهترین شیوه‌ها برای استفاده از تایپ‌های برند شده

برای استفاده مؤثر از تایپ‌های برند شده در تایپ‌اسکریپت، بهترین شیوه‌های زیر را در نظر بگیرید:

مزایای تایپ‌های برند شده

معایب تایپ‌های برند شده

جایگزین‌های تایپ‌های برند شده

در حالی که تایپ‌های برند شده یک تکنیک قدرتمند برای دستیابی به تایپینگ اسمی در تایپ‌اسکریپت هستند، رویکردهای جایگزینی نیز وجود دارد که می‌توانید در نظر بگیرید.

تایپ‌های کدر (Opaque Types)

تایپ‌های کدر شبیه به تایپ‌های برند شده هستند اما راهی صریح‌تر برای پنهان کردن تایپ زیرین ارائه می‌دهند. تایپ‌اسکریپت پشتیبانی داخلی از تایپ‌های کدر ندارد، اما می‌توانید آن‌ها را با استفاده از ماژول‌ها و symbolهای خصوصی شبیه‌سازی کنید.

کلاس‌ها

استفاده از کلاس‌ها می‌تواند رویکردی شیءگرایانه‌تر برای تعریف تایپ‌های متمایز فراهم کند. در حالی که کلاس‌ها در تایپ‌اسکریپت به صورت ساختاری تایپ‌بندی می‌شوند، تفکیک واضح‌تری از مسئولیت‌ها ارائه می‌دهند و می‌توانند برای اعمال محدودیت‌ها از طریق متدها استفاده شوند.

کتابخانه‌هایی مانند `io-ts` یا `zod`

این کتابخانه‌ها اعتبارسنجی پیشرفته تایپ در زمان اجرا را فراهم می‌کنند و می‌توانند با تایپ‌های برند شده ترکیب شوند تا ایمنی هم در زمان کامپایل و هم در زمان اجرا تضمین شود.

نتیجه‌گیری

تایپ‌های برند شده در تایپ‌اسکریپت ابزاری ارزشمند برای افزایش ایمنی تایپ و وضوح کد در یک سیستم تایپ ساختاری هستند. با افزودن یک «برند» به یک تایپ، می‌توانید تایپینگ اسمی را اعمال کرده و از ترکیب تصادفی تایپ‌های مشابه از نظر ساختاری اما متفاوت از نظر منطقی جلوگیری کنید. با وجود اینکه تایپ‌های برند شده مقداری پیچیدگی و سربار به همراه دارند، مزایای افزایش ایمنی تایپ و قابلیت نگهداری کد اغلب بر معایب آن برتری دارد. در سناریوهایی که نیاز دارید اطمینان حاصل کنید یک مقدار به یک تایپ خاص تعلق دارد، صرف‌نظر از ساختار آن، استفاده از تایپ‌های برند شده را در نظر بگیرید.

با درک اصول تایپینگ ساختاری و اسمی، و با به کارگیری بهترین شیوه‌های ذکر شده در این مقاله، می‌توانید به طور مؤثر از تایپ‌های برند شده برای نوشتن کدهای تایپ‌اسکریپت قوی‌تر و قابل نگهداری‌تر استفاده کنید. از نمایش ارزها و شناسه‌ها گرفته تا اعمال محدودیت‌های مختص دامنه، تایپ‌های برند شده مکانیزمی انعطاف‌پذیر و قدرتمند برای افزایش ایمنی تایپ در پروژه‌های شما فراهم می‌کنند.

همچنان که با تایپ‌اسکریپت کار می‌کنید، تکنیک‌ها و کتابخانه‌های مختلف موجود برای اعتبارسنجی و اعمال تایپ را بررسی کنید. استفاده از تایپ‌های برند شده را در کنار کتابخانه‌های اعتبارسنجی زمان اجرا مانند io-ts یا zod برای دستیابی به یک رویکرد جامع برای ایمنی تایپ در نظر بگیرید.