تایپ اسکریپت 2021 در یک مقاله

Learn TypeScript in 15 Minutes

30 خرداد 1400
تایپ اسکریپت 2021 در یک مقاله

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

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

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

راه اندازی اولیه

برای شروع کار پوشه مورد نظر خود را برای تمرین ایجاد کرده و ترمینال را در آن باز کنید. حالا باید دستور زیر را در ترمینال اجرا کنید تا پروژه تایپ اسکریپتی شروع شود:

tsc --init

با این کار فایلی به نام tsconfig برایتان ساخته می شود که حاوی تنظیمات کامپایل تایپ اسکریپت است. همانطور که می دانید تایپ اسکریپت به جاوا اسکریپت کامپایل می شود. من قبلا تمام گزینه های این فایل را در مقاله «فایل tsconfig: نسخه هدف در کامپایل نهایی» از دوره تایپ اسکریپت روکسو توضیح داده ام بنابراین در صورت نیاز می توانید به آن مراجعه کنید. من گزینه های این فایل را در این مقاله بررسی نخواهم کرد.

آشنایی با تایپ ها

مهم ترین قابلیت تایپ اسکریپت این است که به شما اجازه می دهد در جاوا اسکریپت از static typing یا مشخص کردن نوع متغیر ها (رشته، عدد و ...) به صورت ثابت استفاده کنید. در این بخش به بررسی این خصوصیات می پردازیم.

const و let

در ابتدا باید بدانید که const ها در تایپ اسکریپت کاملا ثابت هستند:

const website = "roxo";

website = "roxo.ir";

این کد باعث خطا می شود چرا که website یک بار تعریف شده است و دیگر اجازه تغییر مقدار آن را ندارید. اگر در ویرایشگر کد خود روی website بروید، تایپ آن (نوع داده آن) "roxo" است و رشته نیست! اگر باید مقدار متغیر هایتان را قرار بدهید می توانید از let به جای const استفاده کنید اما مقدار جدید باید تایپ مقداری قبلی را داشته باشد:

let website = "roxo";

website = 13;

این کد به ما خطا می دهد چرا که تایپ website رشته است اما در خط دوم یک عدد را به آن داده ایم.

مسئله بعدی مشخص کردن تایپ ها به صورت صریح (explicit) است. در صورتی که بخواهید تایپ یک متغیر را صریحا مشخص کنید جلوی نام آن دونقطه گذاشته و سپس تایپ مورد نظر را می نویسید:

let website: string = "roxo";




let anotherWebsite = "roxo";

تایپ هر دو متغیر website و anotherWebsite رشته است و تایپ اسکریپت در اکثر اوقات خودش تایپ را حدس می زند اما خط اول از کد بالا روشی برای مشخص کردن صریح آن است. به ترمینال خود برگردید و این بار دستور tsc -w را در آن اجرا کنید. با انجام این کار کدهای تایپ اسکریپت شما در هر بار save شدن، به صورت خودکار به جاوا اسکریپت کامپایل می شوند. کدهای کامپایل شده از مثال بالا بدین شکل خواهند بود:

"use strict";

var website = "roxo";

var anotherWebsite = "roxo";

چرا var؟ به دلیل اینکه تایپ اسکریپت به صورت پیش فرض به ES5 کامپایل می شود. شما می توانید آن را در فایل tsconfig تنظیم کنید.

تایپ در توابع

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

const goToWebsite = (website, protocol) => {

  return protocol + website;

};

این تابع می تواند هر مقداری را به عنوان آرگومان های website و protocol بگیرد. برای مشخص کردن نوع داده های پاس داده شده می توانیم از تایپ اسکریپت استفاده کنیم:

const goToWebsite = (website: string, protocol: "http://" | "https://") => {

  return protocol + website;

};

یعنی پارامتر website حتما string (رشته) است و پارامتر protocol نه تنها رشته است بلکه فقط می تواند یکی از مقادیر //:http یا //:https باشد! اگر کسی برای website عدد پاس بدهد یا برای protocol مقداری غیر از دو مقدار مجاز پاس بدهد خطا می گیرد.

در صورتی که موس خود را روی نام تابع بالا (goToWebsite) ببرید، تایپ این تابع به شکل زیر نمایش داده خواهد شد:

const goToWebsite: (website: string, protocol: "http://" | "https://") => string

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

const goToWebsite = (website: string, protocol: "http://" | "https://"): string => {

  return protocol + website;

};

Interface های تایپ اسکریپت

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

const website = {

  url: "roxo.ir",

  topic: "programming",

  quality: 100,

};

تعریف این شیء بر اساس چیزی جز سلیقه نبوده است. اگر بخواهیم نقشه ای قبلی را آماده کنیم تا تمام اشیائی که از آن پیروی می کنند بر اساس ساختار آن ساخته شوند، باید به سراغ اینترفیس (interface) ها برویم. البته قبل از اینترفیس ها باید بگویم که می توانیم تایپ یک شیء را نیز به صورت صریح مشخص کنیم:

const website: { url: string; topic: string; quality: number } = {

  url: "roxo.ir",

  topic: "programming",

  quality: 100,

};

حالا اگر یکی از این خصوصیات مانند url در شیء website وجود نداشته باشد یا تایپ اشتباهی داشته باشد با خطا روبرو می شویم. انجام این کار برای تک تک اشیاء این کار را بکنیم، به زمان بسیار زیادی نیاز خواهیم داشت. اینترفیس ها این مشکل را حل می کنند. اینترفیس ها اشیاء را توصیف می کنند و قابلیت استفاده چندین باره را دارند:

interface WebsiteInterface {

  url: string;

  topic: string;

  quality: number;

}




const website: WebsiteInterface = {

  url: "roxo.ir",

  topic: "programming",

  quality: 100,

};

WebsiteInterface نام اینترفیس ما است. شما می توانید هر نام دیگری را انتخاب کنید اما یادتان باشد که معمولا نام interface ها با حرف بزرگ شروع می شود. حالا هر شیء ای که از این اینترفیس استفاده کند، باید تمام خصوصیات توصیف شده در آن را داشته باشد در غیر این صورت خطا دریافت می کنیم. اگر می خواهید یکی از این خصوصیات اختیاری باشد (شاید وجود داشته باشد شاید وجود نداشته باشد) باید علامت ؟ را در کنار نامش بگذارید:

interface WebsiteInterface {

  url: string;

  topic: string;

  quality?: number;

}




const website: WebsiteInterface = {

  url: "roxo.ir",

  topic: "programming",

};

من در کنار quality علامت سوال را قرار داده ام و سپس quality را از شیء website حذف کرده ام. با این کار هیچ خطایی نمی گیریم چرا که ؟ به معنی اختیاری بودن آن خصوصیت است. در ضمن شما می توانید توابع را نیز در اینترفیس خود تعریف کنید:

interface WebsiteInterface {

  url: string;

  topic: string;

  quality?: number;

  someFunc(): string;

}




const website: WebsiteInterface = {

  url: "roxo.ir",

  topic: "programming",

  someFunc: () => {

    return "hello from roxo";

  },

};

همانطور که می بینید در اینترفیس گفته ام تابعی به نام someFunc خواهیم داشت که تایپ خروجی آن (مقداری که return می شود) یک رشته خواهد بود. همین کار را نیز در شیء website انجام داده ام بنابراین خطایی نمی گیریم. در ضمن شما می توانید توابع را به شکل زیر نیز در اشیاء تعریف کنید:

const website: WebsiteInterface = {

  url: "roxo.ir",

  topic: "programming",

  someFunc() {

    return "hello from roxo";

  },

};

هیچ تفاوتی ندارد که به کدام شکل متد خود را تعریف کنید (به جز this در arrow function ها).

اپراتور Union

اپراتور union (اتحاد) که به شکل | است به شما اجازه می دهد تایپ ها را با هم ترکیب کنید:

let pageName: string | number;




pageName = 13;

pageName = "roxo";

من برای متغیر pageName گفته ام که می تواند تایپ های رشته یا عدد را داشته باشد بنابراین هر دو مقداری که در خط های دوم و سوم به آن داده ام صحیح هستند. یکی از محبوب ترین کاربرد های اپراتور union بررسی null بودن مقادیر است مثلا:

let errorMessage: string | null = null;

ما در ابتدا در برنامه خود خطایی نداریم بنابراین errorMessage برابر null است اما بعدا اگر خطایی پیش بیاید محتوایی رشته ای در این متغیر قرار خواهد گرفت.

Alias ها

alias نوعی تایپ خاص است که خودمان آن را تعریف می کنیم:

type ID = number;




interface UserInterface {

  id: ID;

  name: string;

}




const user = {

  id: 13,

  name: "Amir",

};

مثلا من در این بخش تایپ خاصی به نام ID را تعریف کرده ام که در اصل یک number است. از این به بعد می توانیم از ID به عنوان یک تایپ عادی استفاده کنیم، همانطور که من در اینترفیس خودم از آن استفاده کرده ام. طبیعتا شما می توانید alias ها را با union و interface ترکیب کنید:

type ID = number;




type UserID = ID | null;




interface UserInterface {

  id: UserID;

  name: string;

}




const user = {

  id: null,

  name: "Amir",

};

با این حساب id کاربر می تواند یا null و یا عدد باشد و کد بالا کاملا صحیح و بدون خطا است.

مقادیر Any و Void و Never و Unknown

void (به معنی «خلاء») تایپ آسانی است:

const sayHello = () => {

  console.log("Hello!");

};

در صورتی که در ویرایشگر خود (مثلا VSCode) موس خود را روی نام تابع بالا ببرید،‌ تایپ تابع بالا به شکل زیر نمایش داده می شود:

const sayHello: () => void

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

تایپ Any بدترین تایپ موجود در تایپ اسکریپت است و بهتر است در حد ممکن از آن دوری شود. any (به معنی «هر چیزی») به سادگی به معنی عدم وجود تایپ خاصی است؛ مثلا متغیری که تایپ آن any باشد می تواند هر مقداری را بگیرد که یعنی اصلا تایپی نداشته است:

let data: any = null;




data = "roxo";

data = 13;

data = undefined;

data = ["roxo", "programming"];

data = { id: 13, name: "Amir" };

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

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

const sayHello = (): never => {

  console.log("Hello!");

};

این کد به شما خطا می دهد چرا که تابع تا انتهای آن اجرا خواهد شد اما کد زیر صحیح است:

const sayHello = (): never => {

  throw Error("Something went wrong");

  console.log("Hello!");

};

زمانی که به throw Error می رسیم اجرای تابع متوقف می شود بنابراین نوع آن never است.

تایپ unknown در نسخه سوم تایپ اسکریپت اضافه شده است و جایگزین بسیار مناسبی برای any می باشد:

let any: any = 10;

let unknown: unknown = 10;




let variableWithAny: string = any;

let variableWithUnknown: string = unknown;

من در ابتدا دو متغیر به نام های any و unknown دارم که تایپ های any و unknown را دارند. در مرحله بعدی متغیری به نام variableWithAny را تعریف می کنم که تایپ رشته ای دارد و متغیر any را به آن پاس می دهم. در این حالت هیچ خطایی نمی گیریم چرا که any هیچ تایپی را در نظر نمی گیرد. در خط بعدی متغیر variableWithUnknown را تعریف کرده ام که باز هم تایپ رشته ای را دارد اما این بار متغیر unknown را به آن پاس داده ام. در این حالت خطا دریافت می کنیم چرا که شما اجازه ندارید متغیری با تایپ unknown را به تایپ های دیگر تغییر بدهید.

به مثال زیر نیز دقت کنید:

let any: any = 10;

let unknown: unknown = 10;




console.log(any.foo());

console.log(unknown.foo());

با صدا زدن تابع foo روی متغیر any هیچ خطایی نمی گیریم اما unknown.foo به ما خطا خواهد داد. امیدوارم متوجه تفاوت any و unknown شده باشید.

Type Assertion یا تبدیل تایپ ها

type assertion با نام type casting نیز شناخته می شود و هر دو به معنی تبدیل تایپ ها به هم دیگر هستند. مثلا اگر بخواهیم تایپ متغیر خود را به تایپ دیگری تبدیل کنیم چطور؟ این کار با اپراتور as انجام می شود. به مثال زیر توجه کنید:

let pageNumber: string = "1";

let numericPageNumber: number = pageNumber;

من در کد بالا شماره صفحه ای را به صورت رشته در متغیر pageNumber ذخیره کرده ام و حالا می خواهم آن را به صورت عدد در متغیر numericPageNumber ذخیره کنم اما طبیعتا کد بالا خطا می دهد. برای تبدیل تایپ ها می توانیم به شکل زیر عمل کنیم:

let pageNumber: string = "1";

let numericPageNumber: number = pageNumber as number;

as number باید داده متغیر pageNumber را به عدد تبدیل کند اما باز هم در کد بالا خطا می گیریم. چرا؟ به دلیل اینکه تایپ اسکریپت تبدیل رشته به عدد را دوست ندارد و فکر می کند اشتباهی رخ داده است. پیام خطای تایپ اسکریپت به شکل زیر است:

Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.ts(2352)

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

let pageNumber: string = "1";

let numericPageNumber: number = (pageNumber as unknown) as number;

حالا دیگر هیچ خطایی نمی گیریم و مقدار موجود در numericPageNumber عدد 1 (نه رشته 1) خواهد بود.

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

تایپ اسکریپت به صورت پیش فرض تقریبا تمام عناصر HTML را می شناسد بنابراین اگر بخواهیم با DOM کار کنیم مشکلی نخواهد بود:

const myElement = document.querySelector(".someClass");

کد بالا هیچ خطایی ندارد اما تایپ اسکریپت نمی تواند به صفحات HTML شما دسترسی داشته باشد بنابراین اصلا نمی داند عنصری به کلاس someClass وجود دارد یا خیر. اگر موس خود را روی نام myElement ببرید تایپ آن به صورت Element نمایش داده می شود. تایپ Element یعنی یک عنصر کلی HTML که بالاترین سطح و تایپ ممکن است. این مسئله یعنی به خصوصیات جزئی تر دسترسی نخواهیم داشت:

const myElement = document.querySelector(".someClass");

console.log(myElement.value);

این کد باعث بروز خطا می شود و تایپ اسکریپت می گوید value روی myElement وجود ندارد چرا که تایپ آن Element است. برای حل این مشکل باید از type casting استفاده کنیم:

const myElement = document.querySelector(".someClass") as HTMLInputElement;

console.log(myElement.value);

تایپ HTMLInputElement به صورت پیش فرض در تایپ اسکریپت وجود دارد و به تایپ اسکریپت می گوید که این عنصر یک Input در HTML است بنابراین value دارد. این مسئله در تمام حالت های کار با DOM صادق است. به طور مثال به event listener زیر توجه کنید:

const myElement = document.querySelector(".someClass");




myElement.addEventListener("blur", event => {

  console.log("Our target value:", event.target.value);

});

خطای اول کد بالا این است که myElement ممکن است null باشد (اصلا عنصری با کلاس someClass نداشته باشیم). خطای دوم نیز مانند همان مورد قبلی است؛ یعنی event یا رویداد دارای تایپ Event است که یک تایپ کلی است و جزئیاتی ندارد بنابراین event.target.value به ما خطا می دهد که value وجود ندارد. ما می توانیم این مشکل را به صورت زیر حل کنیم:

const myElement = document.querySelector(".someClass");




myElement.addEventListener("blur", event => {

  const target = event.target as HTMLInputElement;

  console.log("Our target value:", target.value);

});

با این کار مشکل value را حل کرده ایم اما هنوز خطای اول (خالی بودن myElement) حل نشده است. چند راه برای حل این مشکل وجود دارد. راه اول این است که myElement را نیز type cast کنید:

const myElement = document.querySelector(".someClass") as HTMLInputElement;




myElement.addEventListener("blur", event => {

  const target = event.target as HTMLInputElement;

  console.log("Our target value:", target.value);

});

راه دوم این است که از optional chaining استفاده کنید. برای این کار علامت سوال را قبل از addEventListener اضافه کنید:

const myElement = document.querySelector(".someClass");




myElement?.addEventListener("blur", event => {

  const target = event.target as HTMLInputElement;

  console.log("Our target value:", target.value);

});

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

const myElement = document.querySelector(".someClass");




myElement!.addEventListener("blur", event => {

  const target = event.target as HTMLInputElement;

  console.log("Our target value:", target.value);

});

علامت ! به تایپ اسکریپت می گوید که ما مطمئن هستیم myElement خالی نخواهد بود بنابراین نباید به آن اشکال بگیرد. هر راهی را که دوست داشتید انتخاب کنید.

ساخت کلاس در تایپ اسکریپت

استفاده از کلاس ها در تایپ اسکریپت تفاوت زیادی با جاوا اسکریپت ساده ندارد. تفاوت اصلی معمولا قابلیت های اضافه ای است تایپ اسکریپت در اختیار شما می گذارد. به طور مثال در تایپ اسکریپت می توانید خصوصیات کلاس (class properties) را تایپ دهی کنید و همچنین برای هر کدام از آنها modifier هایی مانند public (عمومی) و private (خصوصی - قابل دسترسی فقط درون خود کلاس) و protected (محافظت شده - قابل استفاده توسط کلاس های فرزند) و readonly (غیر قابل تغییر) را تعیین کنیم (تمام خصوصیات به صورت پیش فرض public هستند):

class User {

  private firstName: string;

  lastName: string;




  constructor(firstName: string, lastName: string) {

    this.firstName = firstName;

    this.lastName = lastName;

  }




  getFullname(): string {

    return this.firstName + " " + this.lastName;

  }

}




const user = new User("Amir", "ZM");

console.log(user.firstName);

console.log(user.lastName);

در کد بالا، اولین دستور log به ما خطا می دهد چرا که firstName یک خصوصیت private است بنابراین فقط درون کلاس به آن دسترسی داریم اما log دوم صحیح است چرا که lastName خصوصیتی public می باشد.

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

در ضمن شما می توانید از interface ها نیز استفاده کنید:

interface UserInterface {

  getFullname(): string;

}




class User implements UserInterface {

  private firstName: string;

  lastName: string;

  static readonly maxAge: number = 50;




  constructor(firstName: string, lastName: string) {

    this.firstName = firstName;

    this.lastName = lastName;

  }




  getFullname(): string {

    return this.firstName + " " + this.lastName;

  }

}




const user = new User("Amir", "ZM");

console.log(user.maxAge);

console.log(User.maxAge);

زمانی که کلاس ما یک اینترفیس را implement می کند یعنی حتما باید خصوصیات درون اینترفیس را داشته باشد (حتما متد getFullName را داشته باشد) اما اگر چیزی بر آن اضافه کند عیبی ندارد. مثلا در این کلاس خصوصیتی استاتیک (static) و readonly به نام maxAge را داریم. خصوصیت استاتیک یعنی خصوصیتی که روی نمونه های ساخته شده از کلاس وجود ندارند بلکه روی خود کلاس هستند. در کد بالا اولین دستور log به ما خطا می دهد چرا که maxAge روی شیء ساخته شده به نام user وجود ندارد اما log دوم بدون مشکل است (U بزرگ به نام کلاس اشاره می کند نه نام شیء ساخته شده از آن).

دیگر مباحث کلاس ها (وراثت و غیره) عملا بین تایپ اسکریپت و جاوا اسکریپت یکی هستند. در صورت نیز به اطلاعات بیشتر به documentation تایپ اسکریپت مراجعه کنید.

Generic ها در تایپ اسکریپت

بهترین راه درک Generic ها در تایپ اسکریپت استفاده از یک مثال است. فرض کنید تابعی داشته باشیم که یک شیء دریافت کرده و به آن یک id تصادفی اضافه کند:

const addId = obj => {

  const id = Math.random();




  return {

    ...obj,

    id,

  };

};




const user = {

  name: "Amir",

};




const result = addId(user);

console.log("Result is: ", result);

با کامپایل کردن این کد و اجرای آن در مرورگر چنین نتیجه ای را می گیریم:

Result is:  { name: 'Amir', id: 0.9644837568987086 }

البته برای بسیاری از شما (بسته به فایل tsconfig) هیچ چیزی کامپایل نمی شود. در عین حال خطایی که همه دریافت می کنیم، خطای any بودن آرگومان obj است. اگر موس خود را روی addId بگیرید (هم در هنگام تعریفش و هم در هنگام صدا زدنش) تایپ مقدار برگردانده شده توسط آن را any می بینید. همچنین اگر موس را روی متغیر result ببرید، آن نیز any خواهد بود اما اگر آن را روی شیء user ببرید تایپ دقیق آن مشخص است. همانطور که گفتم any بسیار بد است بنابراین باید این مشکل را با generic ها حل کنیم.

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

const addId = <T>(obj: T) => {

  const id = Math.random();




  return {

    ...obj,

    id,

  };

};




const user = {

  name: "Amir",

};




const result = addId(user);

console.log("Result is: ", result);

برای تعریف generic از علامت های <> قبل از آرگومان استفاده می کنیم و یک حرف دلخواه با حروف بزرگ را در آن می نویسیم (معمولا قرارداد است که از حرف T استفاده می کنند اما باز هم بسته به سلیقه شما است). حالا می توانید نوع داده obj را روی T تنظیم کنید. یعنی چه؟ یعنی هر داده ای که به عنوان obj پاس داده شود، تایپ obj همان خواهد بود. در حال حاضر اگر موس را روی addId ببرید چنین تایپی به شما نمایش داده می شود:

const addId: <T>(obj: T) => T & {

    id: number;

}

اگر آن را روی result ببرید چنین تایپی را می بینید:

const result: {

    name: string;

} & {

    id: number;

}

و اگر آن را روی addId در محل صدا زده شدنش ببرید (انتهای کد) چنین تایپی را مشاهده می کنید:

const addId: <{

    name: string;

}>(obj: {

    name: string;

}) => {

    name: string;

} & {

    id: number;

}

علامت های <> به معنی generic بودن تابع ما است و درون آن ها می توانید تایپ شیء پاس داده شده را مشاهده کنید. بدین شکل تایپ آرگومان را به صورت پویا تعیین می کنیم. برای واضح تر کردن این موضوع بهتر است برای شیء پاس داده شده به تابع addId یک interface تعریف کنیم:

const addId = <T>(obj: T) => {

  const id = Math.random();




  return {

    ...obj,

    id,

  };

};




interface UserInterface {

  name: string;

}




const user: UserInterface = {

  name: "Amir",

};




const result = addId(user);

console.log("Result is: ", result);

حالا اگر موس خود را روی تابع addId (در بخشی که صدا زده شده است، نه در ابتدا که آن را تعریف کرده ایم) ببرید، چنین تایپی به شما نمایش داده می شود:

const addId: <UserInterface>(obj: UserInterface) => UserInterface & {

    id: number;

}

بنابراین آرگومان ما از نوع UserInterface شناخته شده است! البته شما می توانید در هنگام صدا زدن addId این تایپ generic را به صورت صریح نیز مشخص کنید:

const result = addId<UserInterface>(user);

console.log("Result is: ", result);

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

const result = addId("dadsasuahowihdoawhdoawh");

console.log("Result is: ", result);

با اینکه تایپ generic را در تعریف تابع addId مشخص کرده ایم اما کد بالا کاملا صحیح است. چرا؟ به دلیل اینکه تایپ های generic بسیار کلی هستند (کلمه generic به معنی «کلی» و «عمومی» است) و بالاتر گفتم که generic ها همان داده ورودی را به عنوان تایپ در نظر می گیرند بنابراین هر داده ورودی که به آن پاس بدهیم مورد تایید است. برای حل این مشکل باید تایپ generic خود را به اشیاء محدود کنیم:

const addId = <T extends object>(obj: T) => {

  const id = Math.random();




  return {

    ...obj,

    id,

  };

};




// بقیه کدها




const result = addId("dadsasuahowihdoawhdoawh");

console.log("Result is: ", result);

در اینجا گفته ایم T (تایپ generic ما) نوع داده object (شیء) را extend می کند. به زبان ساده تر T نوعی شیء است. به انجام این کار صدا زدن addId با رشته بالا به ما خطا خواهد داد.

همچنین در نظر داشته باشید که شما می توانید generic ها را روی اینترفیس ها نیز پیاده کنید. چرا؟ شاید قسمتی از interface شما برای اشیاء مختلف تغییر می کند و یک نوع داده ثابت نیست. در این حالت به جای تعریف چندین interface جداگانه می توانید آن قسمت متغیر را با generic ها پیاده سازی کنید:

interface UserInterface<T> {

  name: string;

  data: T;

}




const user: UserInterface = {

  name: "Amir",

};

با انجام این کار شیء user ما با خطا روبرو می شود چرا که خصوصیت data را ندارد. شما نمی توانید این خطا را به شکل زیر تصحیح کنید:

interface UserInterface<T> {

  name: string;

  data: T;

}




const user: UserInterface = {

  name: "Amir",

  data: "SOME DATA",

};

چرا؟ زمانی که از از تایپ generic استفاده می کنیم و هیچ مقدار پیش فرضی وجود ندارد باید دقیقا نوع آن را مشخص کنیم:

interface UserInterface<T> {

  name: string;

  data: T;

}




const user: UserInterface<{ meta: string }> = {

  name: "Amir",

  data: {

    meta: "SOME DATA",

  },

};




const user2: UserInterface<string[]> = {

  name: "Ahmad",

  data: ["foo", "bar", "baz"],

};

همانطور که می بینید من دو شیء user و user2 را دارم و قسمت data در هر دوی آن ها متفاوت است. زمانی که می خواهیم از این اینترفیسِ generic استفاده کنیم باید نوع آن را درون <> مشخص کنیم. در شیء اول نوع آن یک شیء دارای خصوصیتی به نام meta بوده است و در شیء دوم نوع آن آرایه ای از رشته ها می باشد.

در ضمن، شما می توانید بیشتر از یک تایپ Generic داشته باشید:

interface UserInterface<T, V> {

  name: string;

  data: T;

  meta: V;

}




const user: UserInterface<{ meta: string }, string> = {

  name: "Amir",

  data: {

    meta: "SOME DATA",

  },

  meta: "A STRING",

};




const user2: UserInterface<string[], number> = {

  name: "Ahmad",

  data: ["foo", "bar", "baz"],

  meta: 22,

};

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

Enum چیست؟

بهترین راه درک enum استفاده از یک مثال است:

const statuses = {

  notStarted: 0,

  inProgress: 1,

  done: 2,

};




console.log(statuses.inProgress);

نتیجه اجرای این کد عدد 1 خواهد بود. حالا بیایید همین کار را با enum انجام بدهیم:

enum Status {

  NotStarted = "notStarted",

  InProgress = "inProgress",

  Done = "done",

}




console.log(Status.InProgress);

با اجرای این کد باز هم عدد 1 را دریافت می کنیم. آیا متوجه کار enum ها شدید؟ enum ها مقادیر درون خود را از صفر شماره گذاری می کنند. معمولا استفاده از enum ها بدین شکل، دلیل اصلی محبوبیت آن ها نیست بلکه بیشتر به عنوان نوع داده از آن ها استفاده می کنیم:

enum Status {

  NotStarted = "notStarted",

  InProgress = "inProgress",

  Done = "done",

}




let notStrartedStatus: Status = Status.NotStarted;

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

enum Status {

  NotStarted = "notStarted",

  InProgress = "inProgress",

  Done = "done",

}




interface Task {

  id: string;

  status: Status;

}




let notStrartedStatus: Status = Status.NotStarted;




notStrartedStatus = Status.Done;




console.log(Status.InProgress);

با اجرای این کد به جای دریافت یک عدد، رشته مورد نظر را دریافت می کنیم.

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

نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

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