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

در این مقاله قصد داریم تا نکات مربوط به ویژگی type casting در تایپ اسکریپت را باهم بررسی کنیم. برای درک بهتر مقاله، بهتر است با زبان برنامه نویسی تایپ اسکریپت و برنامه نویسی شی‌گرا آشنا باشیم.

Type casting در تایپ اسکریپت چیست؟

Type casting یک ویژگی در تایپ اسکریپت است که به توسعه‌دهندگان اجازه می‌دهد به صراحت تایپ یک مقدار را از یک نوع به نوع دیگر تغییر دهند. استفاده از ویژگی type casting مخصوصاً زمانی که با داده‌های داینامیک کار می‌کنیم یا زمانی که تایپ یک مقدار به‌طور خودکار به درستی استنتاج نمی‌شود، بسیار مفید می‌باشد.

Type casting می‌تواند به یکی از دو روش انجام شود: روش اول این که می‌تواند ضمنی باشد، یعنی زمانی که تایپ اسکریپت عملیات را مدیریت می‌کند، روش دوم هم این که صریح باشد، یعنی زمانی که توسعه‌دهنده conversion را مدیریت می‌کند. casting ضمنی زمانی اتفاق می‌افتد که تایپ اسکریپت یک خطای تایپ را می‌بیند و سعی می‌کند با آن را تصحیح کند.

ویژگی type casting برای انجام عملیات‌های مختلف، از جمله محاسبات ریاضی، داده‌ها، دستکاری‌ها و بررسی‌های سازگاری ضروری است. اما قبل از اینکه بتوانیم به طور موثر از آن استفاده کنیم، باید با برخی از مفاهیم اساسی مانند روابط subtype و supertype، مفهوم type widening و همینطور مفهوم type narrowing آشنا شویم.

مقایسه Type assertion و Type casting

در حالی که این دو اصطلاح اغلب در بین توسعه‌دهندگان به جای یکدیگر استفاده می‌شوند، اما تفاوت ظریفی بین type assertion و type casting در تایپ اسکریپت وجود دارد:

تفاوت کلیدی این است که type assertion صرفاً یک ساختار مربوط به زمان کامپایل است و به تایپ اسکریپت می‌گوید که یک مقدار را به عنوان یک تایپ خاص بدون تأثیر بر رفتار زمان اجرا آن در نظر بگیرد. از طرف دیگر، type casting در واقع داده‌ها را تغییر می‌دهد و می‌تواند بر رفتار زمان اجرا تأثیر بگذارد.

Subtypeها و Supertypeها

یکی از راه‌های طبقه‌بندی تایپ‌ها، تقسیم آن‌ها به -sub و supertypeها است. به طور کلی، یک subtype یک نسخه تخصصی از یک supertype است که ویژگی‌ها و رفتارهای آن را به ارث می‌برد. از طرف دیگر، supertype تایپ کلی‌تری است که اساس subtypeهای متعدد می‌باشد.

سناریویی را در نظر می‌گیریم که در آن ما یک سلسله مراتب کلاس با یک superclass به نام Animal و دو subclass به نام‌های Cat و Dog داریم. در این سناریو، Animal یک supertype است، در حالی که Cat و Dog هر دو subtype هستند. زمانی که ما نیاز داریم یک آبجکت از یک subtype خاص را به عنوان supertype آن در نظر بگیریم یا برعکس، جایی است که باید از type casting استفاده کنیم.

Type widening: از subtype به supertype

Type widening یا upcasting زمانی اتفاق می‌افتد که ما نیاز داریم یک متغیر را از یک subtype به یک supertype تبدیل کنیم. ویژگی type widening معمولاً ضمنی است، به این معنی که توسط تایپ اسکریپت انجام می‌شود، زیرا شامل حرکت از یک دسته محدود به یک دسته گسترده‌تر است. این ویژگی safe می‌باشد و هیچ خطایی ایجاد نمی‌کند، زیرا یک subtype ذاتاً دارای تمام ویژگی‌ها و رفتارهای supertype خود است.

Type narrowing: از supertype به subtype

زمانی که متغیری را از supertype به subtype تبدیل می‌کنیم، type narrowing یا downcasting رخ می‌دهد. تبدیل type narrowing صریح است و برای اطمینان از اعتبار تبدیل نیاز به تأیید تایپ یا بررسی تایپ دارد. این فرآیند می‌تواند باعث ایجاد مشکلاتی در برنامه شود، زیرا همه متغیرهای supertype دارای مقادیری نیستند که با subtype سازگار باشد.

Type casting در تایپ اسکریپت با عملگر as

عملگر as مکانیزم اصلی تایپ اسکریپت برای type casting صریح است. as با سینتکس بصری خود، این امکان را ما می‌دهد تا کامپایلر را در مورد تایپ مورد نظر یک متغیر یا عبارت مطلع کنیم. شکل کلی عملگر as را در مثال زیر مشاهده می‌کنیم:

value as Type

در این مثال value نشان دهنده متغیر یا عبارتی است که می‌توانیم آن را cast کنیم، در حالی که Type نشان دهنده تایپ هدف مورد نظر ما می‌باشد. با استفاده از as، ما به صراحت اعلام می‌کنیم که value از تایپ Type است.

عملگر as زمانی مفید است که با تایپ‌هایی کار می‌کنیم که اجداد مشترکی دارند، از جمله سلسله مراتب کلاس یا پیاده‌سازی interface. این عملگر به ما این امکان را می‌دهد تا نشان دهیم که یک متغیر خاص باید به عنوان یک subtype خاص‌تر در نظر گرفته شود. به عنوان مثال:

class Animal {
  eat(): void {
    console.log('Eating...');
  }
}

class Dog extends Animal {
  bark(): void {
    console.log('Woof!');
  }
}

const animal: Animal = new Dog();
const dog = animal as Dog;
dog.bark(); // Output: "Woof!"

در مثالی که داریم، کلاس Dog کلاس Animal را extend می‌کند. نمونه Dog به یک متغیر animal از نوع Animal اختصاص داده می‌شود. با استفاده از عملگر as، می‌توانیم animal را به عنوان Dog انتخاب کنیم. بنابراین، این امکان را داریم تا به متد bark() که مخصوص کلاس Dog است دسترسی داشته باشیم.

می‌توانیم از عملگر as برای cast به تایپ‌های خاصی استفاده کنیم. این قابلیت زمانی مفید است که ما نیاز به تعامل با تایپی داریم که با تایپ استنتاج شده توسط سیستم استنتاج تایپ تایپ اسکریپت متفاوت است. به عنوان مثال:

function getLength(obj: any): number {
  if (typeof obj === 'string') {
    return (obj as string).length;
  } else if (Array.isArray(obj)) {
    return (obj as any[]).length;
  }
  return 0;
}

تابع getLength یک پارامتر obj از تایپ any را می‌پذیرد. در تابع getLength، عملگر as، پارامتر obj را به یک رشته برای any[] بر اساس نوع آن cast می‌کند. این عملیات این امکان را برای ما فراهم می‌کند تا به ترتیب به ویژگی length که مخصوص رشته‌ها یا آرایه‌ها است، دسترسی داشته باشیم. علاوه بر این، برای این که بیان کنیم یک مقدار می‌تواند یکی از چندین تایپ باشد، می‌توانیم به یک تایپ Union کست کنیم:

function processValue(value: string | number): void {
  if (typeof value === 'string') {
    console.log((value as string).toUpperCase());
  } else {
    console.log((value as number).toFixed(2));
  }
}

تابع processValue یک پارامتر value از نوع string | number را می‌پذیرد که نشان می‌دهد می‌تواند یک رشته یا یک عدد باشد. با استفاده از عملگر as، می‌توانیم value را به string یا number در شرایط مربوطه cast کنیم، و در نتیجه این امکان را داشته باشیم که عملیات‌های تایپ خاص مانند toUpperCase() یا toFixed() را اعمال نماییم.

محدودیت‌های عملگر as

در حالی که عملگر as ابزار قدرتمندی برای type casting در تایپ اسکریپت است، اما محدودیت‌هایی هم دارد. یکی از محدودیت‌ها این است که as صرفاً در زمان کامپایل عمل می‌کند و هیچ‌گونه بررسی زمان اجرا را انجام نمی‌دهد. این بدان معنی است که اگر تایپ cast شده نادرست باشد، ممکن است منجر به ایجاد خطاهای زمان اجرا شود. بنابراین، اطمینان از صحت تایپ cast شده بسیار مهم است.

یکی دیگر از محدودیت‌های عملگر as این است که نمی‌توانیم از آن برای cast بین تایپ‌های نامرتبط استفاده کنیم. سیستم تایپ تایپ اسکریپت برای جلوگیری از casting ناامن، بررسی‌های دقیقی را ارائه می‌کند و از type safety در سراسر پایگاه کد ما اطمینان می‌دهد. در چنین مواردی، می‌توانیم رویکردهای جایگزین، مانند توابع تایید تایپ یا محافظ تایپ را در نظر بگیریم.

وقتی تایپ اسکریپت اجازه as casting را نمی‌دهد

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

ناسازگاری ساختاری با تایپ‌های سفارشی

بررسی تایپ استاتیک تایپ اسکریپت به شدت به سازگاری ساختاری تایپ‌ها، از جمله تایپ‌های سفارشی، متکی است. هنگامی که می‌خواهیم مقداری را با عملگر as ارسال کنیم، کامپایلر سازگاری ساختاری بین تایپ اصلی و تایپ مورد نظر را ارزیابی می‌کند. اگر ویژگی‌های ساختاری دو تایپ سفارشی ناسازگار باشد، تایپ اسکریپت خطایی ایجاد می‌کند که نشان‌دهنده ناامن بودن عملیات casting است. در ادامه مثالی داریم که نمونه‌ای از type casting با خطاهای ناسازگاری ساختاری با استفاده از تایپ‌های سفارشی را نشان می‌دهد:

interface Square {
  sideLength: number;
}

interface Rectangle {
  width: number;
  height: number;
}

const square: Square = { sideLength: 5 };
const rectangle = square as Rectangle; // Error: Incompatible types

تایپ اسکریپت از عملیات as casting جلوگیری می‌کند، زیرا دو تایپ سفارشی Square و Rectangle دارای ویژگی‌های ساختاری متفاوتی هستند. به جای تکیه بر عملیات as casting، یک رویکرد ایمن‌تر وجود دارد و آن ایجاد یک نمونه جدید از تایپ دلخواه و سپس تخصیص دستی مقادیر مربوطه است.

Type guardها با union typeها

Union typeها در تایپ اسکریپت این امکان را به ما می‌دهند تا مقداری را تعریف کنیم که می‌تواند یکی از چندین تایپ ممکن باشد. Type guardها نقش مهمی در محدود کردن تایپ خاصی از یک مقدار در یک بلاک شرطی ایفا می‌کنند و عملیات type-safe را امکان‌پذیر می‌سازند.

با این حال، هنگام تلاش برای cast کردن یک union type با عملگر as، لازم است که تایپ مورد نظر، یکی از تایپ‌های تشکیل‌دهنده union باشد. اگر تایپ مورد نظر در union گنجانده نشده باشد، تایپ اسکریپت اجازه عملیات casting را نمی‌دهد:

type Shape = Square | Rectangle;

function getArea(shape: Shape) {
  if ('sideLength' in shape) {
    // Type guard: 'sideLength' property exists, so shape is of type Square
    return shape.sideLength ** 2;
  } else {
    // shape is of type Rectangle
    return shape.width * shape.height;
  }
}

const square: Shape = { sideLength: 5 };
const area = getArea(square); // Returns 25

در قطعه کد بالا، یک Shape از union type داریم که نشان‌دهنده Square یا Rectangle است. تابع getArea پارامتری از نوع Shape می‌گیرد و باید مساحت را بر اساس شکل خاص محاسبه نماید.

برای تعیین تایپ Shape داخل تابع getArea از type guard استفاده می‌کنیم که وجود ویژگی sideLength را با استفاده از عملگر in بررسی می‌کند. اگر ویژگی sideLength وجود داشته باشد، تایپ اسکریپت تایپ shape را در آن بلاک شرطی به Square محدود می‌کند و این امکان را به ما می‌دهد تا به ویژگی sideLength دسترسی پیدا کنیم.

محدودیت‌های Type assertion

Type assertionها، که با کلمه کلیدی as نشان داده می‌شوند، یک فانکشنالیتی برای نادیده گرفتن تایپ استنتاج شده یا اعلام شده برای یک مقدار ارائه می‌دهند. با این حال، تایپ اسکریپت محدودیت‌های خاصی در type assertionها دارد. به طور خاص، تایپ اسکریپت هنگام محدود کردن یک تایپ از طریق تجزیه و تحلیل جریان کنترل، as casting را ممنوع می‌کند:

function processShape(shape: Shape) {
  if ("width" in shape) {
    const rectangle = shape as Rectangle;
    // Process rectangle
  } else {
    const square = shape as Square;
    // Process square
  }
}

تایپ اسکریپت یک خطا ایجاد می‌کند، زیرا نمی‌تواند تایپ shape را بر اساس type assertionها محدود کند. برای غلبه بر این محدودیت، می‌توانیم یک متغیر جدید در هر شاخه از جریان کنترل معرفی کنیم:

function processShape(shape: Shape) {
  if ("width" in shape) {
    const

 rectangle: Rectangle = shape;
    // Process rectangle
  } else {
    const square: Square = shape;
    // Process square
  }
}

تایپ اسکریپت می‌تواند با تخصیص مستقیم type assertion به یک متغیر جدید، تایپی که محدود شده است را به درستی استنتاج کند.

Discriminated unionها

یک discriminated union گونه‌ای است که نشان‌دهنده مقداری می‌باشد که می‌تواند چندین احتمال داشته باشد. discriminated unionها مجموعه‌ای از تایپ‌های مرتبط را تحت یک parent مشترک ترکیب می‌کنند، که در آن هر تایپ child به‌طور منحصربه‌فردی توسط یک ویژگی متمایز شناسایی می‌شود. این ویژگی متمایز به عنوان یک تایپ literal عمل می‌کند که به تایپ اسکریپت اجازه می‌دهد تا exhaustiveness checking را انجام دهد:

type Circle = {
  kind: 'circle';
  radius: number;
};

type Square = {
  kind: 'square';
  sideLength: number;
};

type Triangle = {
  kind: 'triangle';
  base: number;
  height: number;
};

type Shape = Circle | Square | Triangle;

ما سه تایپ shape را تعریف کرده‌ایم: Circle، Square و Triangle که همه مجموع Shape discriminated union را تشکیل می‌دهند. ویژگی kind متمایزکننده است و با مقدار literal تایپ shape را نشان می‌دهد.

Discriminated unionها زمانی قدرتمندتر می‌شوند که آن‌ها را با type guardها ترکیب کنیم. type guard یک بررسی زمان اجرا است که به تایپ اسکریپت اجازه می‌دهد تا تایپ‌های ممکن را در union بر اساس ویژگی discriminant محدود کند. تابع زیر را در نظر می‌گیریم که مساحت یک شکل را محاسبه می‌کند:

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.sideLength ** 2;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      throw new Error('Invalid shape!');
  }
}

تایپ اسکریپت از ویژگی discriminant، kind، در دستور switch برای انجام exhaustiveness checking استفاده می‌کند. اگر ما تصادفاً یک مورد را حذف کنیم، تایپ اسکریپت یک خطای کامپایل ایجاد کرده و به ما یادآوری می‌کند که باید همه تایپ‌های shape را مدیریت کنیم.

Type casting و discriminatorها

ما می‌توانیم از discriminated unionها برای type casting استفاده کنیم. سناریویی را تصور می‌کنیم که در آن یک generic response object داریم که می‌تواند یکی از این دو تایپ باشد: Success یا Failure. می‌توانیم از یک ویژگی discriminant، status، برای ایجاد تمایز بین این دو استفاده کنیم و بر این اساس type assertionها را انجام دهیم:

type Success = {
  status: 'success';
  data: unknown;
};

type Failure = {
  status: 'failure';
  error: string;
};

type APIResponse = Success | Failure;

function handleResponse(response: APIResponse) {
  if (response.status === 'success') {
    // Type assertion: response is of type Success
    console.log(response.data);
  } else {
    // Type assertion: response is of type Failure
    console.error(response.error);
  }
}

const successResponse: APIResponse = {
  status: 'success',
  data: 'Some data',
};

const failureResponse: APIResponse = {
  status: 'failure',
  error: 'An error occurred',
};

handleResponse(successResponse); // Logs: Some data
handleResponse(failureResponse); // Logs: An error occurred

ویژگی status در کد بالا discriminator است. تایپ اسکریپت تایپ response object را بر اساس مقدار status محدود می‌کند و این امکان را به ما می‌دهد تا بدون نیاز به بررسی تایپ صریح، به ویژگی‌های مربوطه دسترسی داشته باشیم.

Non-casting: عملگر satisfies

عملگر satisfies یک ویژگی جدید در نسخه ۴٫۹ تایپ اسکریپت است که این امکان را برای ما فراهم می‌کند تا بدون casting عبارت، بررسی کنیم که آیا تایپ عبارت مورد نظر با تایپ دیگری مطابقت دارد یا خیر. این کار می‌تواند برای اعتبارسنجی تایپ‌‌های متغیرها و عبارات، بدون تغییر تایپ اصلی آن‌ها مفید باشد.

در ادامه سینتکس استفاده از عملگر satisfies را بررسی می‌کنیم:

expression satisfies type

برنامه‌ای داریم که با استفاده از عملگر satisfies بررسی می‌کند که آیا یک متغیر بزرگ‌تر از ۵ است یا خیر:

const number = 10;
number satisfies number > 5;

اگر تایپ عبارت مطابقت داشته باشد عملگر satisfies مقدار true را return می‌کند و در غیر این صورت، false را برمی‌گرداند. عملگر satisfies ابزار قدرتمندی برای بهبود type safety در کد تایپ اسکریپت ما می‌باشد. این عملگر یک ویژگی نسبتاً جدید است، بنابراین هنوز به اندازه سایر ویژگی‌های تایپ اسکریپت به طور گسترده مورد استفاده قرار نگرفته است. با این حال، این ویژگی ابزار ارزشمندی به حساب می‌آید که می‌تواند در نوشتن کد‌های قابل نگه‌داری و قابل اعتمادتر به ما کمک کند.

تبدیل تایپ‌های داده

در data manipulation، ما همیشه باید داده‌ها را از یک تایپ به تایپ دیگر تبدیل کنیم، و دو تبدیل رایجی که با آن مواجه خواهیم شد عبارتند از: casting یک string به یک number یا تبدیل یک value به string.

casting یک string به یک number

چندین روش برای casting یک string به یک number در تایپ اسکریپت وجود دارد:

استفاده از تابع Number():

let numString: string = '42';
let num: number = Number(numString);

استفاده از عملگر unary +:

let numString: string = '42';
let num: number = +numString;

و در نهایت، استفاده از parseInt() یا parseFloat():

let intString: string = '42';
let int: number = parseInt(intString); 

let floatString: string = '3.14';
let float: number = parseFloat(floatString);

parseInt() و parseFloat() بسیار انعطاف‌پذیرتر هستند، زیرا امکان extract کردن یک عدد از یک رشته که شامل کاراکترهای غیرعددی نیز می‌باشد را فراهم می‌کنند. همچنین، خوب است به این نکته توجه داشته باشیم که اگر نتوانیم رشته را به عنوان یک عدد تجزیه کنیم، همه این متدها NaN (Not a Number) را به دست خواهند آورد.

تبدیل به string

در تایپ اسکریپت برای تبدیل یک مقدار به string می‌توانیم از تابع String() یا متد toString() استفاده کنیم:

let num: number = 42;
let numString: string = String(num);
// or
let numString2: string = num.toString();

let bool: boolean = true;
let boolString: string = String(bool);
// or
let boolString2: string = bool.toString();

هم String() و هم toString() اساسا روی هر تایپی کار می‌کنند و آن را به یک نمایش رشته‌ای تبدیل می‌کنند.

toString() یک متد بر روی خود آبجکت است، در حالی که String() یک تابع سراسری می‌باشد. این دو مورد در بیشتر موارد نتیجه یکسانی خواهند داشت، اما toString() اجازه می‌دهد تا نمایش رشته را با نادیده گرفتن متد در custom typeها سفارشی کنیم:

class CustomType {
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  toString() {
    return `CustomType: ${this.value}`;
  }
}

let custom = new CustomType(42);
console.log(String(custom)); // Output: [object Object]
console.log(custom.toString()); // Output: CustomType: 42

در قطعه کد بالا، String(custom) رفتار خاصی برای CustomType ما ندارد، در حالی که custom.toString() از پیاده‌سازی سفارشی ما استفاده می‌کند.

جمع‌بندی

در این مقاله با روش‌های مختلف type casting در تایپ اسکریپت آشنا شدیم از جمله type assertion با عملگر as، تبدیل تایپ با استفاده از متدهای داخلی مانند String()، Number() و Boolean()، و تفاوت‌های ظریف بین type assertion و type casting.

همچنین در مورد مفاهیمی مانند type guardها و discriminated unionها بیشتر یاد گرفتیم. این مفاهیم به ما این امکان را می‌دهند تا تایپ خاصی را در یک union type بر اساس بررسی‌های زمان اجرا یا ویژگی‌های discriminant محدود نماییم. با استفاده از این تکنیک‌ها، می‌توانیم type safety برنامه‌های خود را به طور موثر بهبود ببخشیم و خطاهای احتمالی را در زمان کامپایل تشخیص دهیم.