بررسی تایپ any در تایپ اسکریپت

any یک تایپ بسیار قدرتمند در تایپ اسکریپت است. تایپ any به ما این امکان را می‌دهد تا با یک value به گونه‌ای رفتار کنیم که گویی به جای تایپ اسکریپت از زبان جاوااسکریپت برای کدنویسی استفاده می‌کنیم. این بدان معنی است که این تایپ، تمام ویژگی‌های تایپ اسکریپت از جمله بررسی تایپ، تکمیل خودکار و safety را غیرفعال می‌کند.

const myFunction = (input: any) => {
  input.someMethod();
};
 
myFunction("abc"); // This will fail at runtime!

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

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

تصور کنید که می‌خواهیم ابزار ReturnType را در تایپ اسکریپت پیاده‌سازی کنیم. این ابزار، یک تایپ تابع می‌گیرد و تایپ مقدار بازگشتی آن را return می‌کند.

ما باید یک تایپ generic ایجاد کنیم که یک تایپ تابع را به عنوان تایپ آرگومان دریافت می‌کند. اگر خودمان را محدود کنیم که از any استفاده نکنیم، ممکن است از unknown استفاده نماییم:

type ReturnType<T extends (...args: unknown[]) => unknown> =
  // Not important for our explanation:
  T extends (...args: unknown[]) => infer R ? R : never;

قسمت مهمی که در این کد برای ما اهمیت دارد، محدودیت T extends (...args: unknown[]) => unknown می‌باشد. چیزی که در این کد می‌گوییم این است که فقط توابعی مجاز هستند که آرایه آرگومان‌های unknown[] را می‌پذیرند و unknown را return می‌کنند.

به نظر می‌رسد این کد برای توابعی که هیچ آرگومانی ندارند، به درستی کار می‌کند:

const myFunction = () => {
  console.log("Hey!");
};
 
type Result = ReturnType<typeof myFunction>;
       
type Result = void

اما به محض اینکه یک آرگومان اضافه نماییم کار خود را متوقف می‌کند:

const myFunction = (input: string) => {
  console.log("Hey!");
};
 
type Result = ReturnType<typeof myFunction>;
Type '(input: string) => void' does not satisfy the constraint '(...args: unknown[]) => unknown'.
  Types of parameters 'input' and 'args' are incompatible.
    Type 'unknown' is not assignable to type 'string'.

در واقع، تنها زمانی کار می‌کند که پارامتر تابع را به input: unknown تغییر دهیم:

const myFunction = (input: unknown) => {
  console.log("Hey!");
};
 
type Result = ReturnType<typeof myFunction>;
       
type Result = void

بنابراین به صورت تصادفی، ما یک تابع ReturnType ایجاد کرده‌ایم که فقط روی توابعی کار می‌کند که unknown را به عنوان آرگومان قبول می‌کنند. این چیزی نیست که ما می‌خواستیم. هدف ما این بود که تابع ReturnType روی هر تابعی به درستی کار کند.

راه حل این است که از any[] به عنوان محدودیت تایپ آرگومان استفاده نماییم:

type ReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;
 
const myFunction = (input: string) => {
  console.log("Hey!");
};
 
type Result = ReturnType<typeof myFunction>;
       
type Result = void

اکنون همانطور که انتظار داشتیم، کد درست کار می‌کند. ما تعریف می‌کنیم که مهم نیست این تابع چه تایپ‌هایی را می‌پذیرد. این بدان معنی است که این تایپ ممکن است هر چیزی باشد.

دلیل safe بودن این موضوع این است که ما عمداً یک تایپ گسترده را تعریف می‌کنیم. به عبارت دیگر، ما به کدی که داریم اعلام می‌کنیم «مهم نیست که تابع چه چیزی را می‌پذیرد، چیزی که مهم است این است که حتما یک تابع باشد». این یک استفاده ایمن از any می‌باشد.

Return کردن تایپ‌های شرطی از توابع Generic

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

const youSayGoodbyeISayHello = (
  input: "hello" | "goodbye"
) => {
  if (input === "goodbye") {
    return "hello";
  } else {
    return "goodbye";
  }
};
 
const result = youSayGoodbyeISayHello("hello");
        
const result: "hello" | "goodbye"

این تابع واقعاً کاری را که ما می‌خواهیم را انجام نمی‌دهد. هدف ما این است، وقتی که "hello" را به آن پاس دادیم، تایپ "goodbye" را return کند. اما در حال حاضر، result به صورت "hello" | "goodbye" نمایش داده می‌شود.

ما می‌توانیم با استفاده از یک تایپ شرطی این مشکل را برطرف کنیم:

const youSayGoodbyeISayHello = <
  TInput extends "hello" | "goodbye"
>(
  input: TInput
): TInput extends "hello" ? "goodbye" : "hello" => {
  if (input === "goodbye") {
    return "hello";
  } else {
    return "goodbye";
  }
};
 
const goodbye = youSayGoodbyeISayHello("hello");
        
const goodbye: "goodbye"
const hello = youSayGoodbyeISayHello("goodbye");
       
const hello: "hello"

ما یک تایپ شرطی به تایپ بازگشتی تابع اضافه کرده‌ایم که منطق زمان اجرا مورد نظر ما را منعکس می‌کند. اگر TInput که از input آرگومان زمان اجرا استنباط می‌شود، "hello" باشد، "goodbye" را return می‌کنیم. در غیر این صورت "hello" را return می‌کنیم.

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

const youSayGoodbyeISayHello = <
  TInput extends "hello" | "goodbye"
>(
  input: TInput
): TInput extends "hello" ? "goodbye" : "hello" => {
  if (input === "goodbye") {
    return "hello";
Type '"hello"' is not assignable to type 'TInput extends "hello" ? "goodbye" : "hello"'.
  } else {
    return "goodbye";
Type '"goodbye"' is not assignable to type 'TInput extends "hello" ? "goodbye" : "hello"'.
  }
};

این طور که به نظر می‌رسد، تایپ اسکریپت نمی‌تواند تایپ شرطی را با منطق زمان اجرا مطابقت دهد. در نتیجه، نمی‌تواند "hello" یا "goodbye" را از تابع return کند.

می‌توانیم با استفاده از as و دادن تایپ شرطی صحیح به آن، این مشکل را برطرف نماییم:

const youSayGoodbyeISayHello = <
  TInput extends "hello" | "goodbye"
>(
  input: TInput
): TInput extends "hello" ? "goodbye" : "hello" => {
  if (input === "goodbye") {
    return "hello" as TInput extends "hello"
      ? "goodbye"
      : "hello";
  } else {
    return "goodbye" as TInput extends "hello"
      ? "goodbye"
      : "hello";
  }
};

همینطور می‌توانیم با extract کردن آن منطق به یک تایپ generic معمول، کدی که داریم را بهتر کنیم:

type YouSayGoodbyeISayHello<
  TInput extends "hello" | "goodbye"
> = TInput extends "hello" ? "goodbye" : "hello";
 
const youSayGoodbyeISayHello = <
  TInput extends "hello" | "goodbye"
>(
  input: TInput
): YouSayGoodbyeISayHello<TInput> => {
  if (input === "goodbye") {
    return "hello" as YouSayGoodbyeISayHello<TInput>;
  } else {
    return "goodbye" as YouSayGoodbyeISayHello<TInput>;
  }
};

اما در این شرایط، استفاده از as any منطقی‌تر است:

const youSayGoodbyeISayHello = <
  TInput extends "hello" | "goodbye"
>(
  input: TInput
): TInput extends "hello" ? "goodbye" : "hello" => {
  if (input === "goodbye") {
    return "hello" as any;
  } else {
    return "goodbye" as any;
  }
};

البته باید به این نکته توجه داشته باشیم که این موضوع باعث می‌شود که ویژگی type-safe تابع کم‌تر شود. در عوض می‌توانیم به‌طور تصادفی "bonsoir" را از تابع return کنیم.

اما در چنین شرایطی، اغلب بهتر است از as any استفاده کنیم و یک unit test برای رفتار این تابع اضافه نماییم. به دلیل محدودیت‌های تایپ اسکریپت در بررسی این موارد، این موضوع اغلب به type safety نزدیک‌تر است.

جمع‌بندی

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

دیدگاه‌ها:

افزودن دیدگاه جدید