توابع در تایپ اسکریپت [TypeScript]

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

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

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

این چهارمین مقاله از سری مقالات راهنمای جامع تایپ اسکریپت است. برای مشاهده بقیه بخش ها، فهرست مقالات را ببینید.

اکسپرشن های نوع تابع

به ساده ترین روش توصیف یک تابع، اکسپرشن نوع تابع یا function type expression گفته می شود. ترکیب و سینتکس آن شبیه توابع arrow است:

function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}
 
function printToConsole(s: string) {
  console.log(s);
}
 
greeter(printToConsole);

ترکیب (a: string) => void یعنی یک تابع با یک پارامتر نوع رشته و بدون خروجی. اگر نوع پارامتر را تعیین نکنیم به صورت پیش فرض any در نظر گرفته می شود.

توجه کنید که اسم پارامتر ضروری است. مثلا تابع (string) => void به معنای یک تابع با پارامتری به اسم string نوع any و بدون خروجی است.

و البته می توانیم از alias نوع هم استفاده کنیم:

type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}

تعریف به روش Call Signature

در جاوا اسکریپت توابع علاوه بر اینکه قابل فراخوانی هستند، می توانند دارای ویژگی هم باشند. اکسپرشن نوع تابع اجازه تعریف ویژگی برای تابع را نمی دهد. اگر بخواهیم تابعی تعریف کنیم که دارای ویژگی است، باید آن را به صورت Call Signature تعریف کنیم:

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}
 
function myFunc(someArg: number) {
  return someArg > 3;
}
myFunc.description = "default description";
 
doSomething(myFunc);

توجه کنید که ترکیب و سینتکس آن کمی با اکسپرشن نوع تابع متفاوت است و از : بجای => استفاده می شود.

تعریف به روش Construct Signature

در جاوا اسکریپت می توانیم توابع را با new هم فراخوانی کنیم. برای توصیف چنین تابعی می توانیم از Construct Signature استفاده کنیم:

type SomeConstructor = {
  new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

برخی از آبجکت ها را در جاوا اسکریپت ( مثل Date ) می توان هم با new و هم بدون آن فراخوانی کرد. برای توصیف این حالت می توانیم Call Signature و Construct Signature را ترکیب کنیم:

interface CallOrConstruct {
  new (s: string): Date;
  (n?: number): number;
}

توابع جنریک (Generic)

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

function firstElement(arr: any[]) {
  return arr[0];
}

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

در تایپ اسکریپت جنریک یا Generic ها برای توصیف ارتباط بین دو مقدار استفاده می شوند. برای اینکار یک پارامتر نوع در تعریف تابع می نویسیم:

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

با اضافه کردن یک پارامتر نوع به اسم Type به تابع فوق و استفاده از آن در دو جای مختلف (ورودی و خروجی تابع)، بین این دو قسمت ارتباط برقرار کردیم.

با فراخوانی این تابع نوع خروجی برگشتی وابسته به نوع آرایه ورودی خواهد بود:

// s: string
const s = firstElement(["a", "b", "c"]);

// n: number
const n = firstElement([1, 2, 3]);

// u: undefined
const u = firstElement([]);

استنتاج و تشخیص نوع

توجه کنید که در تابع بالا نوع Type را ما مشخص نکردیم و تایپ اسکریپت خودش آن را تشخیص داد. می توانیم چندین پارامتر نوع هم تعریف کنیم. برای مثال:

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}
 
// n: string
// parsed: number[]
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

در مثال فوق تایپ اسکریپت می تواند هر دو پارامتر نوع Input و Output را از روی ورودی آرایه رشته ها و خروجی عددی تابع، تشخیص دهد.

محدودیت گذاری (Constraints)

تا اینجا توابع جنریکی نوشتیم که می توانند با هر نوع مقداری کار کنند. گاهی اوقات نیاز داریم که فقط نوع های به خصوصی را استفاده کنیم. در این شرایط می توانیم با یک constraint پارامتر های نوع را محدود به فقط نوع های خاصی کنیم.

برای مثال تابعی را در نظر بگیرید ویژگی length دو ورودی اش را بررسی می کند و هر کدام که بزرگ تر باشد را بر می گرداند:

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}
 
// longerArray: number[]
const longerArray = longest([1, 2], [1, 2, 3]);

// longerString: 'alice' | 'bob'
const longerString = longest("alice", "bob");

// خطا! اعداد چنین ویژگی ندارند
const notOK = longest(10, 100);

در مثال بالا با constraint extends { length: number } پارامتر های ورودی را فقط محدود به مقادیری محدود کردیم که ویژگی length داشته باشند.

کار با مقادیر محدود شده

این یک خطای متداول هنگام کار با constraint های جنریک است:

function minimumLength<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj; // اوکی
  } else {
    return { length: minimum }; // خطا! نوع خروجی درست نیست
  }
}

ممکن است تابع بالا به نظر درست برسد چون Type به { length: number } محدود شده است و خروجی { length: minimum } هم با این محدودیت سازگار است. مشکل اینجاست که نوع خروجی تابع دقیقا Type تعیین شده است، نه هر آبجکتی که با محدودیت تعریف شده سازگاری دارد.

اگر تایپ اسکریپت برای تابع بالا خطا نمی گرفت موقع اجرای کد ممکن بود به چنین مشکلی برخورد کنیم:

const arr1 = minimumLength([1, 2, 3], 2);
const arr2 = minimumLength([1, 2, 3], 6);

// متغیر اول اوکی است چون یک آرایه واقعی است
arr1.slice(0);

// موقع اجرای این خط به خطا بر می خوریم چون
// این یک آرایه واقعی نیست و چنین متدی ندارد
arr2.slice(0);

مشخص کردن پارامترهای نوع

تایپ اسکریپت معمولا می تواند پارامتر های نوع خودش را تشخیص دهد. ولی همیشه آن چیزی که ما نیاز داریم نیست. برای مثال تصور کنید تابعی داریم که مجموع دو آرایه مختلف را بر می گرداند:

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

در حالت عادی چنین کدی با وجود اینکه درست است ولی در تایپ اسکریپت خطا می دهد.

const arr = combine([1, 2, 3], ["hello"]); // خطا!

راه حل این کار مشخص کردن دقیق Type به صورت زیر است:

const arr = combine<string | number>([1, 2, 3], ["hello"]);

راهنمای نوشتن توابع جنریک خوب

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

ارسال پارامتر های نوع از بالا به پایین

کد زیر نوع یک تابع را به دو شکل مختلف تعریف می کند:

function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}
 
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}
 
// a: number (خوب)
const a = firstElement1([1, 2, 3]);

// b: any (بد)
const b = firstElement2([1, 2, 3]);

شاید در نگاه اول یکسان بنظر برسند ولی firstElement1 روش بسیار بهتری برای توصیف نوع تابع مدنظر ما است چون نوع خروجی آن توسط تایپ اسکریپت Type تشخیص داده می شود.

سعی کنید تا جای ممکن بجای extends از خود پارامتر نوع استفاده کنید.
(مثلا <Type extends any[]>(arr: Type) را به صورت <Type>(arr: Type[]) نوشتیم.

استفاده از تعداد پارامتر های نوع کمتر

توابع زیر را در نظر بگیرید:

function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}
 
function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}

در filter2 ما پارامتر نوع Func را تعریف کردیم که هیچ مقادیری را به هم مرتبط نمی کند. این همیشه یک علامت خطر است، چون بی هیچ دلیلی حتما باید موقع فراخوانی این تابع، به صورت دستی نوع آن را بنویسیم. Func به جز بدتر کردن خوانایی کد، کار خاصی نمی کند.

سعی کنید از تعداد پارامتر های نوع کمتری استفاده کنید.

پارامتر های نوع حداقل باید دو بار استفاده شوند

گاهی اوقات فراموش می کنیم که یک تابع شاید به جنریک بودن نیازی نداشته باشد:

function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}
 
greet("world");

به راحتی نسخه ساده تری می توانیم بنویسیم:

function greet(s: string) {
  console.log("Hello, " + s);
}

به یاد داشته باشید که پارامتر های نوع برای ارتباط دادن بین نوع مقادیر مختلف هستند. اگر یک پارامتر نوع فقط در یک جا استفاده شده باشد چیزی را به هم مرتبط نمی کند.

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

پارامتر های اختیاری

توابع در جاوا اسکریپت می توانند تعداد متغیری از پارامتر ها را دریافت کنند. برای مثال به متد toFixed اعداد می توانیم به صورت اختیاری تعداد رقم را بدهیم:

function f(n: number) {
  console.log(n.toFixed()); // 0 arguments
  console.log(n.toFixed(3)); // 1 argument
}

در تایپ اسکریپت پارامتر های اختیاری توابع را می توانیم با علامت ? مشخص کنیم:

function f(x?: number) {
  // ...
}
f(); // OK
f(10); // OK

با وجود اینکه نوع x به صورت number تعیین شده ولی در واقع تایپ اسکریپت نوع number | undefined را به آن می دهد چون پارامتر های وارد نشده در تایپ اسکریپت مقدار undefined دارند.

همچنین می توانیم مقدار پیش فرض پارامتر را به این صورت مشخص کنی:

function f(x = 10) {
  // ...
}

در این حالت نوع x فقط number است چون اگر مقدار آن وارد نشود یا undefined باشد با عدد 10 جایگزین می شود. توجه کنید که موقع فراخوانی تابع می توان برای پارامتر های اختیاری undefined نیز وارد کرد:

declare function f(x?: number): void;

f();
f(10);
f(undefined);

پارامتر های اختیاری در Callback

اشتباه زیر هنگام استفاده و تعریف پارامتر های اختیاری توابع، متداول است:

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  }
}

معمولا برخی از افراد از نوشتن index? منظورشان این است که هر دو حالت زیر را مجاز نمایند:

myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));

در حالی که در واقعیت یعنی تابع callback می تواند بدون پارامتر index فراخوانی شود:

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    // پارامتر دوم اختیاری است و وارد نمی کنم
    callback(arr[i]);
  }
}

و موقع استفاده، چنین خطایی ممکن است رخ دهد:

myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed()); // خطا!
});

راه حل این است که نیازی به اختیاری کردن پارامتر index نیست. در جاوا اسکریپت می توانیم توابع را با ورودی های بیشتر از پارامتر های آن فراخوانی کنیم و ورودی های اضافی نادیده گرفته می شوند. تایپ اسکریپت هم به همین صورت عمل می کند. توابع با تعداد پارامتر های کمتر همیشه می توانند جایگزین توابع (هم نوع) با پارامتر های بیشتر شوند.

موقع نوشتن یک تابع برای callback سعی کنید هیچ پارامتری را اختیاری نکنید مگر اینکه قصد داشته باشید تابع را بدون آن پارامتر فراخوانی کنید.

Overload های توابع

برخی از توابع را می توان با تعداد و نوع های مختلفی از پارامتر ها فراخوانی کرد. نمونه چنین تابعی Date است که هم می تواند با یک پارامتر (timestamp) و هم می تواند با سه پارامتر (سال، ماه، روز) فراخوانی شود.

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

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3); // خطا!

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

توجه کنید که موقع فراخوانی تابع فقط اورلود های آن را می توانیم استفاده کنیم یعنی در مثال بالا با وجود اینکه در تعریف خود تابع (نه اورلود هایش) پارامتر دوم و سوم اختیاری هستند، نمی توانیم این تابع را مثلا با دو پارامتر فراخوانی کنیم. حتما باید با یکی از اورلود هایش فراخوانی شود (یک پارامتر یا سه پارامتر).

چند مثال دیگر بررسی می کنیم تا بهتر با تعریف Overload توابع آشنا شویم:

function fn(x: string): void;
function fn() {
  // ...
}

fn(); // خطا!
// فقط طبق اورلود تابع که یک پارامتر ضروری دارد را می توانیم فراخوانی کنیم

در مثال زیر نوع پارامتر خود تابع با یکی از اورلود هایش سازگار نیست:

function fn(x: boolean): void;
function fn(x: string): void; // خطا!
function fn(x: boolean) {}

در مثال زیر نوع خروجی یکی از اورلود ها با خود تابع سازگار نیست:

function fn(x: string): string;
function fn(x: number): boolean; // خطا
function fn(x: string | number) {
  return "oops";
}

تعریف پیاده سازی تابع از بیرون قابل مشاهده نیست، فقط اورلود ها قابل مشاهده هستند و باید همیشه حداقل دو اورلود یا بیشتر داشته باشیم.

نوشتن overload های بهتر

مثل جنریک ها، برای اورلود ها هم اصولی وجود دارند که بهتر است آنها را رعایت کنید. پیروی از اصول زیر باعث می شوند توابع شما قابل فهم تر شوند و استفاده از آنها راحت تر باشد.

چنین تابعی را در نظر بگیرید که طول یک رشته یا یک آرایه را بر می گرداند:

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
  return x.length;
}

تابع بالا درست است و می توانیم آن را با یک رشته یا یک آرایه فراخوانی کنیم، ولی نمی توانیم از یک مقدار که ممکن است رشته یا آرایه باشد استفاده کنیم چون تایپ اسکریپت در هر لحظه فقط می تواند یک اورلود را تشخیص دهد:

len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]); // خطا!

برای تابع بالا می توانیم به راحتی نسخه اورلود نشده اش را بنویسیم:

function len(x: any[] | string) {
  return x.length;
}

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

همیشه سعی کنید از نوع union بجای overload تابع استفاده کنید.

تعریف this در یک تابع

تایپ اسکریپت می تواند نوع this را داخل یک تابع تشخیص دهد. برای مثال:

const user = {
  id: 123,
 
  admin: false,
  becomeAdmin: function () {
    this.admin = true;
  },
};

در مثال فوق تایپ اسکریپت می تواند تشخیص دهد که this داخل تابع becomeAdmin در واقع آبجکت بالایی یا همان user است. در خیلی از مواقع این کافی است ولی گاهی اوقات لازم می شود که نوع this تابع را هم تعیین کنیم.

در جاوا اسکریپت نمی توانیم پارامتری تعریف کنیم که اسمش this باشد، تایپ اسکریپت از این قضیه استفاده کرده و امکان تعریف نوع this را به این صورت ممکن می کند:

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

فقط توجه کنید که در صورت استفاده از توابع arrow چون این توابع this ندارند، طبیعتا با خطا مواجه خواهید شد:

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(() => this.admin); // خطا

سایر نوع هایی که باید بشناسیم

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

نوع void

نوع void مقدار خروجی توابعی را توصیف می کند که هیچ مقداری بر نمی گردانند:

function noop() {
  return;
}

در جاوا اسکریپت توابعی که هیچ مقداری برنگردانند، به طور خودکار مقدار خروجی شان undefined می شود، ولی در تایپ اسکریپت void و undefined باهم فرق دارند. نوع void یعنی خروجی تابع مشخص نیست و مقدار خروجی نباید استفاده می شود.

توجه: void همان undefined نیست! اطلاعات بیشتر

نوع object

نوع خاص object هر مقداری که پایه (رشته، عدد و …) نباشد را توصیف می کند. این نوع با نوع های {} و Object متفاوت است. به احتمال زیاد هیچ وقت از نوع Object استفاده نخواهید کرد.

توجه: object همان Object نیست! همیشه از object استفاده کنید.

توابع هم در جاوا اسکریپت آبجکت هستند و ویژگی دارند. به همین خاطر در تایپ اسکریپت هم یک تابع می تواند object باشد.

نوع unknown

نوع unknown هم مانند نوع any به معنای هر مقداری است. با این تفاوت که امن تر است چون اجازه انجام هیچ عملیاتی روی unknown نداریم:

function f1(a: any) {
  a.b(); // OK
}
function f2(a: unknown) {
  a.b(); // خطا!
}

مثال دیگر برای استفاده از unknown

function safeParse(s: string): unknown {
  return JSON.parse(s);
}
 
// باید مراقب باشیم!
const obj = safeParse(someRandomString);

نوع never

برخی از توابع هرگز مقداری بر نمی گردانند. مثل تابع زیر که هدف آن خطا دادن است:

function fail(msg: string): never {
  throw new Error(msg);
}

نوع never همچنین زمانی رخ می دهد که تایپ اسکریپت تشخیص بدهد دیگر هیچ نوعی در یک union وجود ندارد:

function fn(x: string | number) {
  if (typeof x === "string") {
    // ...
  } else if (typeof x === "number") {
    // ...
  } else {
    x; // نوع 'never'!
  }
}

نوع Function

نوع Function ویژگی هایی مثل bind ، call ، apply و غیره را توصیف می کند که در همه توابع وجود دارند. همه مقادیر این نوع قابل فراخوانی هستند و مقدار خروجی آن ها any است.

function doSomething(f: Function) {
  return f(1, 2, 3);
}

بخاطر نوع خروجی آن یعنی any بهتر است از آن دوری کنیم. برای تعریف نوع یک تابع کلی، معمولا بهتر است از () => void استفاده کنیم.

پارامتر های Rest

در صورت استفاده از پارامتر های rest می توانیم نوع آن را به این صورت در تایپ اسکریپت مشخص کنیم:

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}

const a = multiply(10, 1, 2, 3, 4);

نوع پارامتر rest باید حتما یک نوع آرایه باشد و در صورتی که نوع آن را مشخص نکنیم، تایپ اسکریپت بجای any نوع آن را any[] تشخیص می دهد.

قدم بعدی

در این مطلب با توابع در تایپ اسکریپت آشنا شدیم. در مقاله بعدی از سری آموزش جامع تایپ اسکریپت، نوع های آبجکت را بررسی خواهیم کرد:

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

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


کامنت ها