محدود سازی یا Narrowing در تایپ اسکریپت [TypeScript]
منبع: https://rasanika.com
تایپ اسکریپت مسیر های مختلف اجرای کد را دنبال می کند و می تواند با بررسی و تحلیل کد ما، محدود ترین و دقیق ترین نوع ممکن را برای یک مقدار در یک موقعیت معین، تشخیص دهد. به این بررسی ها (یا به اصطلاح: گارد یا محافظ نوع) و تبدیل نوع ها به نوع های محدودتر و دقیق تر، narrowing یا محدود سازی گفته می شود.
این سومین مقاله از سری مقالات راهنمای جامع تایپ اسکریپت است. برای مشاهده بقیه بخش ها، فهرست مقالات را ببینید.
تصور کنید یک تابع به اسم padLeft
داریم:
function padLeft(padding: number | string, input: string): string {
...
}
اگر padding
عدد باشد، آن را به عنوان تعداد فاصله هایی که به اول input
خواهیم داد، در نظر می گیریم. ولی اگر رشته باشد، خود padding
را به اول input
اضافه خواهیم کرد. برای شروع، حالت اول را کد نویسی می کنیم:
function padLeft(padding: number | string, input: string): string {
return " ".repeat(padding) + input; // ⚠ خطا!
}
در کد بالا تایپ اسکریپت خطا می دهد که نوع ورودی تابع repeat
فقط می تواند عدد باشد ولی نوع padding
هم می تواند عدد باشد و هم ممکن است رشته باشد، پس نمی توانیم آن را به تابع repeat
بدهیم.
راه حل این مشکل چک کردن و مطمئن شدن از عدد بودن padding
است. پس همین کار را انجام می دهیم:
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
شرط typeof padding === "number"
در کد فوق را تایپ اسکریپت تحلیل می کند و متوجه می شود اگر این شرط صحیح باشد، داخل بلوک آن، نوع متغیر padding
قطعا عدد است. پس داخل این بلوک می توانیم آن را به تابع repeat
دهیم.
تایپ اسکریپت می تواند بخش های مختلف کد را به این صورت تحلیل کند و نوع دقیق تر و محدود تر را تشخیص دهد. به شرط فوق (typeof ... === "..."
) اصطلاحا یک گارد یا محافظ نوع گفته می شود.
محدود سازی با typeof
همانطور که می دانید در جاوا اسکریپت می توانیم با استفاده از عملگر typeof
نوع یک مقدار را در لحظه اجرا به دست آوریم. این عملگر یکی از مقادیر زیر را بر می گرداند:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
تایپ اسکریپت در بسیاری از موقعیت ها با بررسی این اطلاعات می تواند نوع یک مقدار را به طور دقیق تر بداند. در واقع typeof
یک محافظ و گارد نوع محسوب می شود. یک مثال دیگر:
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) { // ⚠ خطا! هنوز ممکن است نال باشد
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
...
}
}
در مثال فوق شرط typeof strs === "object"
نوع strs
را به string[] | null
محدود می کند چون مقدار typeof
هم آرایه و هم null
در جاوا اسکریپت برابر object
است. راه حل این مشکل محدود سازی با بررسی درستی یا truthiness
است.
محدود سازی با درستی / نادرستی
در جاوا اسکریپت می توانیم هر اکسپرشنی (expression) را با &&
و ||
و !
در یک شرط if
استفاده کنیم. نوع یک مقدار، داخل شرط if می تواند boolean
نباشد. در این صورت جاوا اسکریپت آن را تبدیل به boolean
می کند.
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
به مقادیری که در چنین شرایطی تبدیل به true
می شوند truthy و به مقادیری که تبدیل به false
می شوند falsy گفته می شود. همه مقادیر در جاوا اسکریپت به جز مقادیر زیر truthy هستند:
false
0
-0
0n
(صفر نوع BigInt)""
(رشته خالی)null
undefined
NaN
(غیر عدد)
با استفاده از تابع Boolean
یا عملگر !!
می توانیم هر مقداری را به true
یا false
تبدیل کنیم:
Boolean("hello"); // true
!!"hello"; // true
حالا با استفاده از این محدود سازی و گارد، می توانیم تابع printAll
در مثال بالا را اصلاح کنیم:
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") { // فقط به آرایه محدود شد.
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
توجه: هنگام تبدیل نوع مقادیر به boolean
باید حواسمان به همه مقادیر falsy باشد:
فرض کنید می خواهیم تابعی تعریف کنیم که رشته بودن ورودی اش را کنترل می کند:
function isString (value: string | null) {
if (value) { // اشتباه
return true;
}
return false;
}
اگر به تابع فوق رشته خالی بدهیم می گوید رشته نیست! چون رشته خالی یک مقدار falsy است و ما آن را در نظر گرفتیم. نسخه اصلاح شده:
function isString (value: string | null) {
if (value === "" || value) {
return true;
}
return false;
}
البته مثال فوق فقط جهت نشان دادن این مشکل بود و برای بررسی رشته بودن یا نبودن عملگر typeof
مناسب تر است:
function isString (value: string | null) {
return typeof value === "string";
}
محدود سازی با برابر بودن
تایپ اسکریپت از switch
و همچنین عملگر های ===
، !==
، ==
و !=
نیز برای محدود سازی نوع ها استفاده می کند:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// در این قسمت نوع هر دو متغیر رشته است
}
}
در مثال فوق در صورتی پارامتر x
و y
طبق عملگر ===
برابر باشند، قطعا نوع شان نیز برابر است. تنها نوع مشترک بینشان هم رشته است، پس نوع هر دو پارامتر، داخل بلوک if
قطعا رشته است. این حالت نیز برای تایپ اسکریپت قابل تشخیص است.
محدود سازی با عملگر in
در جاوا اسکریپت عملگر in
برای بررسی موجود بودن یک ویژگی در یک آبجکت استفاده می شود. تایپ اسکریپت هم با تحلیل این عملگر می توان نوع ها را محدودتر و دقیق تر کند:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim(); // اوکی
}
return animal.fly(); // اوکی
}
در تابع بالا اگر ویژگی swim
داخل animal
باشد یعنی نوعش Fish
است و در غیر این صورت نوعش Bird
است. چون داخل شرط if خط return
داریم نیازی به استفاده از else
هم نیست و تایپ اسکریپت قادر است آن را تشخیص بدهد.
محدود سازی با instanceof
جاوا اسکریپت دارای یک عملگر برای بررسی instance یا نمونه بودن یک مقدار از مقدار دیگری است. مثلا اگر d
را به این صورت باشد: const d = new Date()
عبارت d instanceof Date
درست (true) است. تایپ اسکریپت از این عملگر هم به عنوان گارد/محافظ نوع استفاده می کند:
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString()); // اوکی
} else {
console.log(x.toUpperCase()); // اوکی
}
}
محدود سازی با اختصاص مقدار
همانطور که قبلا مشاهده کردیم، تایپ اسکریپت می تواند نوع یک متغیر را با بررسی مقداری که به آن داده ایم تشخیص دهد:
let x = Math.random() < 0.5 ? 10 : "hello world!";
// نوع ایکس: رشته | عدد
console.log(x);
x = 1;
// نوع ایکس: عدد
console.log(x);
x = "reza";
// نوع ایکس: رشته
console.log(x);
توجه: با وجود اینکه نوع x
در مثال فوق با هر اختصاص مقدار، دقیق تر و محدود تر می شود ولی چون نوع اصلی آن string | number
است، همچنان امکان اختصاص هم مقدار رشته و هم مقدار عدد به آن وجود دارد.
تعریف گارد با استفاده از predicate
تا این لحظه فقط محدود سازی با استفاده از گارد های نوع موجود در خود جاوا اسکریپت را بررسی کردیم. قابلیت دیگری هم وجود دارد که با استفاده از آن می توانیم خودمان با بررسی ویژگی های یک آبجکت، نوع آن را به تایپ اسکریپت بگوییم.
به این قابلیت گارد/محافظ نوع تعریف شده توسط کاربر گفته می شود. برای تعریف چنین گاردی یک تابع تعریف می کنیم که خروجی آن یک predicate نوع است:
function isFish(pet: Fish | Bird): pet is Fish {
if ("swim" in pet) {
return true;
}
return false;
}
عبارت pet is Fish
همان predicate نوع است. اول اسم پارامتر مدنظر سپس is
و سپس نوع مدنظر را می نویسیم. مقدار خروجی تابع هم باید true
یا false
باشد.
حالا تایپ اسکریپت می تواند از این تابع به عنوان یک گارد نوع استفاده کرده و نوع ها را محدودتر نماید:
const pet: Fish | Bird = ...;
if (isFish(pet)) {
pet.swim(); // اوکی
} else {
pet.fly(); // اوکی
}
همچنین برای فیلتر کردن یک آرایه:
const pets: (Fish | Bird)[] = [...];
const fish = pets.filter(isFish); // fish: Fish[]
// حتی محدودیت بیشتر:
const someFish = pets.filter((pet): pet is Fish => {
if (pet.name === "sharkey") return false;
return isFish(pet);
});
علاوه بر این، برای محدود سازی نوع کلاس ها از ترکیب this is
می توانیم استفاده کنیم که در مطالب بعدی بررسی خواهیم کرد.
نوع never
زمانی که با استفاده از محدود کردن و narrowing همه نوع های ممکن را حذف کرده باشیم، تایپ اسکریپت نوع never
را در نظر می گیرید. یعنی حالتی که قرار نیست اتفاق بیفتد. نوع never
را به هر نوع دیگری می توان اختصاص داد ولی هیچ نوع دیگری را نمی توان به نوع never
اختصاص داد. (به جز خودش)
به عنوان مثال نوع Shape
را به این صورت در نظر بگیرید:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
و فرض کنید چنین تابعی برای محاسبه مساحت تعریف کرده ایم:
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
اگر بعد از مدتی بخواهیم یک نوع جدید به Shape
اضافه کنیم، ممکن است فراموش کنیم که نحوه محاسبه مساحت شکل جدید را نیز به تابع getArea
اضافه کنیم.
ولی با اختصاص به نوع never
می توانیم مطمئن شویم که اگر همه حالت های ممکن را ننوشته بودیم، تایپ اسکریپت به ما اخطار بدهد. پس بند default
را به کد بالا اضافه می کنیم:
function getArea(shape: Shape) {
switch (shape.kind) {
...
default:
const _check: never = shape;
return _check;
}
}
در این صورت کد زیر خطا خواهد داد:
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _check: never = shape; // ⚠ خطا!
return _check;
}
}
البته این کد را می توانستیم بدون اضافه کردن default
هم به این صورت بنویسیم:
function getArea(shape: Shape): number { // نوع خروجی صراحتا عدد تعریف شده است
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
با مشخص کردن نوع خروجی این تابع به عنوان عدد، در صورتی که همه مسیر های کد به عدد ختم نشود، تایپ اسکریپت خطا خواهد داد. یعنی با اضافه کردن نوع جدید به Shape
یک مسیر جدید و در نتیجه خطا خواهیم داشت.
قدم بعدی
در این مطلب تا حدودی با امکان محدود سازی یا narrowing آشنا شدیم. در مقاله بعدی از سری آموزش جامع تایپ اسکریپت، کمی بیشتر نوع توابع را بررسی خواهیم کرد:
توابع در تایپ اسکریپت [TypeScript]
rasanika.comتوابع یکی از اساسی ترین بخش ها در هر برنامه ای هستند. توابع لوکال، توابع وارد شده از ماژول های دیگر و یا متد های یک کلاس، نمونه های مختلف تعریف و استفاده توابع هستند. در واقع تابع هم یک مقدار است و مانند هر مقدار دیگری، تایپ اسکریپت روش های مختلفی برای توصیف نوع یک تابع دارد. در ادامه با نحوه تعریف و توصیف نوع توابع مختلف در تایپ اسکریپت آشنا خواهیم شد. این چهارمین مقاله از سری مقالات راهنمای جامع تایپ اسکریپت است. برای مشاهده بقیه بخش ها، فهرست مقالات را ببینید. اکسپرشن های نوع تابع به ساده تری
برای یادگیری بهتر حتما آموزش های تایپ اسکریپتی رسانیکا و سایر سایت ها را هم ببینید. البته صرفا با مطالعه یا دیدن دوره های آموزشی نمی توان مهارت لازم را کسب کرد و سعی کنید همراه با این مطالعات پروژه های کاملی هم بسازید.
برای تست کد تایپ اسکریپت در مرورگر هم می توانید از TS Playground استفاده کنید.