تایپ اسکریپت یک سیستم تایپ قوی را معرفی میکند که توسعهدهندگان را قادر میسازد تا انواع متغیرها، پارامترهای تابع، مقادیر بازگشتی و غیره را تعریف و اجرا کنند. سیستم تایپ تایپ اسکریپت چک کردن استاتیک تایپ را فراهم میکند و این امکان را به ما میدهد تا قبل از زمان اجرا، خطاهای احتمالی را شناسایی کرده و از ایجاد آنها جلوگیری کنیم.
در این مقاله قصد داریم تا نکات مربوط به ویژگی type casting در تایپ اسکریپت را باهم بررسی کنیم. برای درک بهتر مقاله، بهتر است با زبان برنامه نویسی تایپ اسکریپت و برنامه نویسی شیگرا آشنا باشیم.
Type casting یک ویژگی در تایپ اسکریپت است که به توسعهدهندگان اجازه میدهد به صراحت تایپ یک مقدار را از یک نوع به نوع دیگر تغییر دهند. استفاده از ویژگی type casting مخصوصاً زمانی که با دادههای داینامیک کار میکنیم یا زمانی که تایپ یک مقدار بهطور خودکار به درستی استنتاج نمیشود، بسیار مفید میباشد.
Type casting میتواند به یکی از دو روش انجام شود: روش اول این که میتواند ضمنی باشد، یعنی زمانی که تایپ اسکریپت عملیات را مدیریت میکند، روش دوم هم این که صریح باشد، یعنی زمانی که توسعهدهنده conversion را مدیریت میکند. casting ضمنی زمانی اتفاق میافتد که تایپ اسکریپت یک خطای تایپ را میبیند و سعی میکند با آن را تصحیح کند.
ویژگی type casting برای انجام عملیاتهای مختلف، از جمله محاسبات ریاضی، دادهها، دستکاریها و بررسیهای سازگاری ضروری است. اما قبل از اینکه بتوانیم به طور موثر از آن استفاده کنیم، باید با برخی از مفاهیم اساسی مانند روابط subtype و supertype، مفهوم type widening و همینطور مفهوم type narrowing آشنا شویم.
در حالی که این دو اصطلاح اغلب در بین توسعهدهندگان به جای یکدیگر استفاده میشوند، اما تفاوت ظریفی بین type assertion و type casting در تایپ اسکریپت وجود دارد:
as
استفاده میکند.String()
، Number()
، Boolean()
و غیره انجام دهیم.تفاوت کلیدی این است که type assertion صرفاً یک ساختار مربوط به زمان کامپایل است و به تایپ اسکریپت میگوید که یک مقدار را به عنوان یک تایپ خاص بدون تأثیر بر رفتار زمان اجرا آن در نظر بگیرد. از طرف دیگر، type casting در واقع دادهها را تغییر میدهد و میتواند بر رفتار زمان اجرا تأثیر بگذارد.
یکی از راههای طبقهبندی تایپها، تقسیم آنها به -sub و supertypeها است. به طور کلی، یک subtype یک نسخه تخصصی از یک supertype است که ویژگیها و رفتارهای آن را به ارث میبرد. از طرف دیگر، supertype تایپ کلیتری است که اساس subtypeهای متعدد میباشد.
سناریویی را در نظر میگیریم که در آن ما یک سلسله مراتب کلاس با یک superclass به نام Animal
و دو subclass به نامهای Cat
و Dog
داریم. در این سناریو، Animal
یک supertype است، در حالی که Cat
و Dog
هر دو subtype هستند. زمانی که ما نیاز داریم یک آبجکت از یک subtype خاص را به عنوان supertype آن در نظر بگیریم یا برعکس، جایی است که باید از type casting استفاده کنیم.
Type widening یا upcasting زمانی اتفاق میافتد که ما نیاز داریم یک متغیر را از یک subtype به یک supertype تبدیل کنیم. ویژگی type widening معمولاً ضمنی است، به این معنی که توسط تایپ اسکریپت انجام میشود، زیرا شامل حرکت از یک دسته محدود به یک دسته گستردهتر است. این ویژگی safe میباشد و هیچ خطایی ایجاد نمیکند، زیرا یک subtype ذاتاً دارای تمام ویژگیها و رفتارهای supertype خود است.
زمانی که متغیری را از supertype به subtype تبدیل میکنیم، type narrowing یا downcasting رخ میدهد. تبدیل type narrowing صریح است و برای اطمینان از اعتبار تبدیل نیاز به تأیید تایپ یا بررسی تایپ دارد. این فرآیند میتواند باعث ایجاد مشکلاتی در برنامه شود، زیرا همه متغیرهای supertype دارای مقادیری نیستند که با subtype سازگار باشد.
عملگر 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
ابزار قدرتمندی برای type casting در تایپ اسکریپت است، اما محدودیتهایی هم دارد. یکی از محدودیتها این است که as
صرفاً در زمان کامپایل عمل میکند و هیچگونه بررسی زمان اجرا را انجام نمیدهد. این بدان معنی است که اگر تایپ cast شده نادرست باشد، ممکن است منجر به ایجاد خطاهای زمان اجرا شود. بنابراین، اطمینان از صحت تایپ cast شده بسیار مهم است.
یکی دیگر از محدودیتهای عملگر as
این است که نمیتوانیم از آن برای cast بین تایپهای نامرتبط استفاده کنیم. سیستم تایپ تایپ اسکریپت برای جلوگیری از casting ناامن، بررسیهای دقیقی را ارائه میکند و از type safety در سراسر پایگاه کد ما اطمینان میدهد. در چنین مواردی، میتوانیم رویکردهای جایگزین، مانند توابع تایید تایپ یا محافظ تایپ را در نظر بگیریم.
مواردی وجود دارد که تایپ اسکریپت مخالفتهایی را مطرح میکند و از دادن مجوز برای 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، یک رویکرد ایمنتر وجود دارد و آن ایجاد یک نمونه جدید از تایپ دلخواه و سپس تخصیص دستی مقادیر مربوطه است.
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ها، که با کلمه کلیدی 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ها مجموعهای از تایپهای مرتبط را تحت یک 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 را مدیریت کنیم.
ما میتوانیم از 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
محدود میکند و این امکان را به ما میدهد تا بدون نیاز به بررسی تایپ صریح، به ویژگیهای مربوطه دسترسی داشته باشیم.
عملگر 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 در تایپ اسکریپت وجود دارد:
استفاده از تابع 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()
یا متد 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 برنامههای خود را به طور موثر بهبود ببخشیم و خطاهای احتمالی را در زمان کامپایل تشخیص دهیم.