در جاوااسکریپت، توابع به عنوان first-class citizenها در نظر گرفته می‌شوند. این به این معنی است که می‌توانیم آن‌ها را مانند هر نوع متغیر دیگری از جمله اعداد، رشته‌ها و آرایه‌ها مدیریت کنیم. این ویژگی به توابع اجازه می‌دهد تا مانند یک پارامتر به توابع دیگر ارسال شوند، از توابع return گردند و برای استفاده بعدی به متغیرها تخصیص داده شوند.

این ویژگی در کدهای asynchronous بسیار مورد استفاده قرار می‌گیرد، جایی که توابع اغلب به توابع asynchronous انتقال پیدا می‌کنند، که معمولا به عنوان callbackها نامیده می‌شوند. اما استفاده از این ویژگی در تایپ اسکریپت می‌تواند کمی دشوار باشد.

تایپ اسکریپت مزایای خارق‌العاده افزودن تایپ استاتیک و بررسی‌های transpile کردن را ارائه می‌دهد و می‌تواند به ما کمک کند تا تایپ‌های متغیرهایی که انتظار داریم در توابع خود داشته باشیم را بهتر مستند کنیم. بدیهی است که استفاده از این توابع یک موضوع ضروری است. با این حال، این سوال مطرح می‌شود: چگونه باید این توابع را بنویسیم و چگونه یک تابع را در تایپ اسکریپت ارسال کنیم؟

ما در این مقاله قصد داریم تا تابع در تایپ اسکریپت و نحوه ارسال آن به عنوان پارامتر را باهم بررسی نماییم.

تایپ تابع تایپ اسکریپت چیست؟

اکثر توسعه‌دهندگان تایپ اسکریپت با تایپ متغیرهای ساده آشنا هستند، اما ساخت یک تایپ برای یک تابع ممکن است کمی پیچیده باشد.

یک تایپ تابع از تایپ آرگومان‌هایی که تابع قبول می‌کند، و تایپ مقداری که تابع آن را return می‌کند تشکیل می‌شود. برای بررسی این موضوع می‌توانیم یک مثال بسیار ساده را در نظر بگیریم:

const stringify = (el : any) : string => { return el + "" } 
const numberify = (el : any) : number => { return Number(el) }

let test = stringify;
test = numberify;

اگر این کد در جاوااسکریپت پیاده‌سازی شود، به خوبی کار می‌کند و مشکلی ندارد.

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

- Type '(el: any) => number' is not assignable to type '(el: any) => string'.
-   Type 'number' is not assignable to type 'string'.

پیام خطایی که ایجاد شده است این است که تابع stringify و numberify قابل تعویض نیستند. ما نمی‌توانیم آن‌ها را به جای یکدیگر به متغیر test نسبت دهیم، زیرا دارای تایپ‌های متضاد می‌باشند.

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

ما می‌توانیم تایپ‌های بازگشتی را تغییر دهیم تا تئوری که داریم درست باشد:

const stringify = (el : any) : number => { return 1 } 
const numberify = (el : any) : number => { return Number(el) }

let test = stringify;
test = numberify;

اکنون همانطور که انتظار داریم کد بالا کار می‌کند. تنها کاری که انجام دادیم این است که تابع stringify را تغییر دادیم تا با تایپ return تابع numberify مطابقت داشته باشد.

تعریف تایپ توابع در تایپ اسکریپت

ما در تایپ اسکریپت می‌توانیم یک تایپ تابع را با استفاده از کلمه کلیدی type تعریف کنیم. کلمه کلیدی type در تایپ اسکریپت این امکان را به ما می‌دهد تا بتوانیم شکل داده‌ها را مشخص نماییم:

type AddOperator = (a: number, b: number) => number;

در کد بالا، یک type alias به نام AddOperator با استفاده از کلمه کلیدی type تعریف می‌کنیم. این مثال یک تایپ تابع را نشان می‌دهد که دو پارامتر (a و b) از نوع number را می‌گیرد و یک value از نوع number را return می‌کند.

روش دیگر برای تعریف تایپ تابع، استفاده از سینتکس interface است. تایپ تابعی که interface Add در ادامه تولید می‌کند با همان تایپ تابعی که AddOperator دارد یکسان می‌باشد:

// Using interface for function type
interface Add {
  (a: number, b: number): number;
}

تعریف صریح ساختارهای تابع، درک روشنی از ورودی‌ها و خروجی‌های مورد انتظار را فراهم می‌کند. این امر خوانایی کد را افزایش می‌دهد، به عنوان مستندات عمل کرده و نگه‌داری کد را ساده‌تر می‌کند.

یکی دیگر از مزیت‌های اصلی تعریف تایپ‌های توابع، توانایی یافتن خطاها در زمان کامپایل است. تایپ استاتیک تایپ اسکریپت تضمین می‌کند که توابع به تایپ‌های مشخص شده پایبند هستند و از خطاهای زمان اجرا ناشی از تایپ‌های نامناسب پارامترها یا تایپ‌های بازگشتی نامعتبر جلوگیری می‌کند.

در مثال زیر، کامپایلر تایپ اسکریپت یک خطایی ایجاد می‌کند که نشان دهنده عدم تطابق بین تایپ‌های بازگشتی مورد انتظار و واقعی است:

// Function violating the parameter type
const addFn: AddOperator = (a, b) => `${a}${b}`;
// Error: Type 'string' is not assignable to type 'number'

تعریف تایپ‌های توابع به محیط‌های توسعه (IDEها) اجازه می‌دهد تا تکمیل خودکار را به صورت دقیق ارائه دهند.

IntelliSense، یک ویژگی قدرتمند ارائه شده توسط IDEهای مدرن است که پیشنهادات درست برای تکیمل کد را در اختیار توسعه‌دهندگان قرار می‌دهد. تایپ‌های توابع اطلاعات صریحی را در مورد تایپ پارامترها ارائه می‌دهند و درک ورودی‌های مورد انتظار را آسان‌تر می‌کنند. زمانی که شروع به تایپ نام یا پارامترهای تابع می‌کنیم، IntelliSense از تایپ‌های تعریف شده برای پیشنهاد گزینه‌های معتبر، به منظور به حداقل رساندن خطاها و صرفه‌جویی در زمان استفاده می‌کند.

استفاده از pass-by-reference تایپ اسکریپت یا جاوااسکریپت برای توابع

در جاوااسکریپت و یا تایپ اسکریپت، درک مفاهیم pass-by-value و pass-by-reference برای کار با توابع و دستکاری داده‌ها بسیار مهم است. تایپ‌های Primitive مانند Boolean، null، undefined، String و Number به عنوان pass-by-value در نظر گرفته می‌شوند، در حالی که آبجکت‌ها که شامل آرایه‌ها و توابع می‌باشد به عنوان pass-by-reference در نظر گرفته می‌شوند.

در تایپ اسکریپت هنگامی که یک آرگومان به تابع ارسال می‌شود، مقدار pass-by-value به این معنی است که یک کپی از متغیر ایجاد می‌گردد و هر گونه تغییری که در تابع ایجاد می‌شود، بر روی متغیر اصلی تأثیری نمی‌گذارد. در مثال زیر، مقدار متغیر a را در داخل تابع تغییر می‌دهیم، اما مقدار متغیر a در خارج تغییر نمی‌کند، زیرا a با pass-by-value به تابع ارسال می‌شود:

const numberIncrement = (a: number) => {
    a = a + 1;
    return a;
}
let a = 1;
let b = numberIncrement(a);
console.log(`pass by value -> a = ${a} b = ${b}`);
// pass by value -> a = 1 b = 2

هنگامی که یک آرگومان آبجکت یا آرایه به یک تابع ارسال می‌شود، به عنوان pass-by-reference تلقی می‌شود. آرگومان به عنوان یک رفرنس کپی می‌شود، نه خود آبجکت. بنابراین، تغییرات در ویژگی‌های آرگومان در داخل تابع در آبجکت اصلی منعکس می‌گردد. در مثال زیر، مشاهده می‌کنیم که تغییر آرایه orignalArray در داخل تابع، orignalArray در خارج از تابع را تحت تاثیر قرار می‌دهد:

const arrayIncrement = (arr: number[]): void => {
  arr.push(99);
};
const originalArray: number[] = [1, 2, 3];
arrayIncrement(originalArray);
console.log(`pass by ref => ${originalArray}`);  // pass by ref => 1,2,3,99

برخلاف برخی تصورات اشتباه، حتی اگر رفرنس به آبجکت برای pass-by-reference کپی شده باشد، خود رفرنس همچنان با value ارسال می‌شود. اگر رفرنس آبجکت دوباره در داخل تابع تخصیص داده شود، آبجکت اصلی خارج از تابع را تحت تأثیر قرار نمی‌دهد.

مثال زیر تخصیص مجدد یک رفرنس آرایه از originalArray در داخل تابع را نشان می‌دهد و آبجکت اصلی آن تحت تأثیر قرار نمی‌گیرد:

const arrayIncrement = (arr: number[]): void => {
  arr = [...arr, 99];
  console.log(`arr inside the function => ${arr}`); 
};
//arr inside the function => 1,2,3,99
const originalArray: number[] = [1, 2, 3];
arrayIncrement(originalArray);
console.log(`arr outside => ${originalArray}`);  // arr outside => 1,2,3

الگوهای پیشرفته: genericها و overload تابع

Generic در تایپ اسکریپت راهی برای نوشتن توابعی ارائه می‌دهد که می‌توانند با هر نوع داده‌ای کار کنند. به عنوان مثال:

function identity<T>(arg: T): T {
  return arg;
}
console.log(identity("Hello, TypeScript!"));
console.log(identity(99));

در این مثال، تابع identity می‌تواند مقادیری از هر تایپ را بپذیرد و return کند. این انعطاف‌پذیری به ما اجازه می‌دهد تا توابعی بنویسیم که با انواع داده‌ها سازگار باشند.

ساخت توابع با قابلیت استفاده مجدد و سازگار با استفاده از genericها

ما می‌توانیم با استفاده از genericها توابع بسیاری با قابلیت استفاده مجدد و سازگاری ایجاد کنیم که با انواع داده‌های مختلف کار می‌کنند.

فرض کنید می‌خواهیم یک تابع کاربردی برای جستجوی المنت‌ها در یک آرایه بر اساس یک معیار خاص ایجاد کنیم. استفاده از generic به تابع اجازه می‌دهد تا با انواع مختلفی از آرایه‌ها کار کند و معیارهای جستجوی مختلف را در خود جای دهد:

function findElements<T>(arr: T[], filterFn: (item: T) => boolean): T[] {
  return arr.filter(filterFn);
}

در مثال بالا، یک تابع generic به نام findElements ایجاد می‌کنیم که یک آرایه arr و یک تابع filterFn را به عنوان پارامتر می‌گیرد. پارامتر filterFn یک تابع callback است که تعیین می‌کند آیا یک المنت، معیار خاص مورد نظر را برآورده می‌کند یا خیر و یک Boolean برمی‌گرداند.

در ادامه چند مثال وجود دارد که در آن‌ها از تابعی که بالاتر درمورد آن صحبت کردیم، برای پرداختن به تایپ‌های number، object و معیارهای مختلف جستجو استفاده می‌کنیم. ما از این تابع برای فیلتر کردن اعداد فرد از یک آرایه اعداد و همینطور فیلتر کردن محصولات ارزان قیمت از آرایه‌ای از محصولات استفاده می‌کنیم تا انعطاف‌پذیری آن با انواع داده‌های مختلف را بررسی کنیم:

const arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const oddNumbers: number[] = findElements(arr, (num) => num % 2 === 1);
console.log("Odd Numbers:", oddNumbers);
interface Product {
  name: string;
  price: number;
}
const products: Product[] = [
  { name: "Phone", price: 400 },
  { name: "Laptop", price: 1000 },
  { name: "Tablet", price: 300 }
];
const cheapProducts =  findElements(products, (p) => p.price < 500);
console.log('cheap products:', cheapProducts);

استفاده از generic این تابع را قابل استفاده مجدد کرده و سازگاری آن را بالا می‌برد، و آن را برای آرایه‌هایی از تایپ‌های primitive یا آبجکت‌های سفارشی بدون به خطر انداختن type safety قابل استفاده می‌کند.

overload توابع

overload توابع به ما اجازه می‌دهد تا چندین تایپ signature برای یک تابع واحد ارائه کنیم. این موضوع به ویژه زمانی مفید است که یک تابع بتواند ترکیبات مختلفی از انواع آرگومان‌ها را بپذیرد.

برای استفاده از overload تابع، باید چندین overload signature و یک implementation تعریف کنیم. overload signature پارامترها و تایپ‌های بازگشتی یک تابع را بدون گنجاندن یک بدنه implementation واقعی مشخص می‌کند:

// Overload signature
function greeting(person: string): string;
function greeting(persons: string[]): string;
// Implementation of the function
function greeting(input: string | string[]): string {
  if (Array.isArray(input)) {
    return input.map(greet => `Hello, ${greet}!`).join(' ');
  } else {
    return `Hello, ${input}!`;
  }
}
// Consume the function
console.log(greeting('Bob'));
console.log(greeting(['Bob', 'Peter', 'Sam']));

در مثال بالا، ما تابعی ایجاد می‌کنیم که overload تابع را نشان می‌دهد که پارامترهایی اعم از یک رشته یا آرایه‌ای از رشته‌ها را می‌پذیرد. این تابع، به نام greeting، دارای دو overload برای رسیدگی به این سناریوها می‌باشد. implementation بررسی می‌کند که آیا پارامتر input یک رشته است یا یک آرایه از رشته‌ها، و بر این اساس عمل مناسب را برای هر مورد انجام می‌دهد.

ما می‌توانیم با استفاده از genericها توابع همه‌کاره‌ای بسازیم که با انواع داده‌های مختلف کار می‌کنند.

استفاده از آرگومان‌های number در تایپ اسکریپت

جالب اینجاست که بسیاری از زبان‌های دیگر این تایپ تابع‌ها را بر اساس تایپ آرگومان‌ها، تایپ‌های بازگشتی و تعداد آرگومان‌های تابع ایجاد می‌کنند. به عنوان مثال:

const stringify = (el : any, el2: number) : number => { return 1 } 
const numberify = (el : any) : number => { return Number(el) }

let test = stringify;
test = numberify;

توسعه‌دهندگانی که با زبان‌های دیگر آشنا هستند ممکن است فکر کنند که مثال‌های تابع بالا قابل تعویض نیستند، زیرا دو signature تابع متفاوت می‌باشند.

اگرچه این مثال هیچ خطایی ایجاد نکرده و در تایپ اسکریپت به درستی کار می‌کند زیرا تایپ اسکریپت آنچه را که duck typing نامیده می‌شود را پیاده‌سازی می‌کند.

در duck typing، تایپ اسکریپت بررسی می‌کند که آیا ساختار تابع اختصاص داده شده با تایپ مورد انتظار مطابق با پارامترهای تابع و تایپ بازگشتی سازگار است یا خیر. در این مثال، هر دو stringify و numberify ساختار یکسانی دارند: تابعی که یک یا چند پارامتر، از تایپ any را می‌گیرد و یک عدد را return می‌کند. با وجود تفاوت در تعداد پارامترهای بین دو تابع، به دلیل duck typing امکان نوشتن این کد را می‌دهد.

نکته مهمی که باید به آن توجه داشته باشیم این است که تعداد آرگومان‌ها در تعریف تایپ برای توابع در تایپ اسکریپت استفاده نمی‌شود.

بررسی مثال‌های مربوط به توابع در تایپ اسکریپت

اکنون، ما دقیقاً می‌دانیم که چگونه می‌توانیم برای توابع خود تایپ بسازیم. ما باید اطمینان حاصل کنیم وقتی یک تابع را در تایپ اسکریپت ارسال می‌کنیم حتما دارای تایپ مخصوص به خود باشد. به عنوان مثال:

const parentFunction = (el : () ) : number => { return el() }

مثال بالا کار نمی‌کند، اما آنچه ما نیاز داریم را نشان می‌دهد.

ما باید آن را به عنوان یک callback به یک تابع parent ارسال کنیم، که می‌تواند بعدا فراخوانی شود. بنابراین، کاری که باید انجام دهیم به این صورت است که:

پس از انجام این کار، مثال ما اکنون به شکل زیر می‌باشد:

const parentFunction = (el : () => any ) : number => { return el() }

این مثال خاص به آرگومان نیاز ندارد، اما اگر نیاز داشته باشد، به صورت زیر خواهد بود:

const parentFunction = (el : (arg: string) => any ) : number => { return el("Hello :)") } 

این انجمن تعداد زیادی تایپ متن باز با کیفیت بالا را که معمولاً در تایپ اسکریپت استفاده می‌شود را به نام Definitely Typed ارائه می‌کند که می‌تواند به ما در سرعت بخشیدن به کدنویسی کمک کند.

جمع‌بندی

در این مقاله سعی کردیم تا ارسال تابع به عنوان یک پارامتر در تایپ اسکریپت را باهم بررسی کنیم. callbackها معمولاً به این روش متکی هستند، بنابراین اغلب در هر پایگاه کد تایپ اسکریپتی، استفاده زیادی از callbackها را مشاهده خواهیم کرد.