محدود سازی یا Narrowing در تایپ اسکریپت [TypeScript]

محدود سازی یا Narrowing در تایپ اسکریپت

تایپ اسکریپت مسیر های مختلف اجرای کد را دنبال می کند و می تواند با بررسی و تحلیل کد ما، محدود ترین و دقیق ترین نوع ممکن را برای یک مقدار در یک موقعیت معین، تشخیص دهد. به این بررسی ها (یا به اصطلاح: گارد یا محافظ نوع) و تبدیل نوع ها به نوع های محدودتر و دقیق تر، 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 (غیر عدد)

  • document.all

با استفاده از تابع 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 آشنا شدیم. در مقاله بعدی از سری آموزش جامع تایپ اسکریپت، کمی بیشتر نوع توابع را بررسی خواهیم کرد:

برای یادگیری بهتر حتما آموزش های تایپ اسکریپتی رسانیکا و سایر سایت ها را هم ببینید. البته صرفا با مطالعه یا دیدن دوره های آموزشی نمی توان مهارت لازم را کسب کرد و سعی کنید همراه با این مطالعات پروژه های کاملی هم بسازید.

برای تست کد تایپ اسکریپت در مرورگر هم می توانید از TS Playground استفاده کنید.


کامنت ها