پرش به مطلب اصلی

مباحث پایه

Basic Types

تو JavaScript هم تایپ داریم اما خیلی سختگیرانه ازشون استفاده نمی‌کنیم. مثلاً این کد درسته:

let variable = 23;
variable = "Bijan";

همونطور که می‌بینید اول اومدیم یه مقدار عددی ریختیم داخل متغیر و بعد یه مقدار Stringیی. خطایی هم بهمون نداد.

اما تو TypeScript می‌تونیم نوع متغیر رو مشخص کنیم و بعدش دیگه حق نداریم مقداری به جز اون تایپ درونش ذخیره کنیم:

let variable: number = 23;
variable = "Bijan"; // TS2322: Type string is not assignable to type number

در ادامه چند مثال از تایپ‌های پایه میارم که خیلی کاربردی هستن:

const name: string = "Bijan";
const age: number = 23;
const isDeveloper: boolean = true;

const nothing: null = null;
const nothing2: undefined = undefined;

const freinds: string[] = ["Ross", "Rachel", "Chandler", "Monica", "Joey", "Phoebe"];
const friends2: Array<string> = ["Geller", "Green", "Bing", "Geller", "Tribbiani", "Buffay"];
اطلاع

برای تایپ آرایه میشه از هر دو Syntax بالا استفاده کرد و فرقی با هم ندارن.

Types by Inference

اگه زمانی که متغیر رو تعریف می‌کنیم، همون موقع هم مقداردهی‌ش کنیم، خودِ TypeScript تایپش رو می‌فهمه. بنابراین دیگه نیاز نیست دستی بهش تایپ بدیم.

let message: string = "Hello, friend!";
let message = "Hello, friend!";
اطلاع

به این ویژگی میگن Inference که تو ساخت تایپ‌های پیچیده هم کاربرد داره.

کاربرد

اینجا چندتا مثال میزنم از به کار بردن تایپ‌ها تو جاهای مختلف پروژه.

Variables
let name: string;
Function parameters
function minus(a: number, b: number) {
return a - b;
}
Function return type
function minus(a, b): number {
return a - b;
}
Class fields
class Circle {
radius: number;

constructor(radius) {
this.radius = radius;
}
}
Constructors
class Circle {
radius;

constructor(radius: number) {
this.radius = radius;
}
}

Custom Types

علاوه بر تایپ‌های پایه، می‌تونیم تایپ‌ها رو با هم ترکیب کنیم و تایپ‌های پیچیده بسازیم.

به‌عنوان مثال می‌تونیم یه تایپ جدید به اسم Id تعریف کنیم که می‌تونه number یا string باشه:

type Id = number | string;

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

type Status = "active" | "inactive";

استفاده کردن از این تایپ‌ها مثل تایپ‌های پایه‌ست:

const id1: Id = 23;
const id2: Id = "ab6c8bd0";

const status: Status = "inactive";
اطلاع

اسم تایپ‌ها باید PascalCase باشه.

Object Types

تو TypeScript، می‌تونیم با استفاده از interface یا type، شکل و شمایل objectها رو مشخص کنیم:

interface Person {
firstName: string;
lastName: string;
}

type Person = {
firstName: string;
lastName: string;
};

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

به‌عنوان مثال، با type میشه یه سری تایپ خیلی پیشرفته درست کرد که با interface نمیشه.

طبق مستندات TypeScript، شما باید به طور پیش‌فرض از interface استفاده کنید و زمانی سراغ type برید که واقعاً بهش نیاز دارید.

Intersection

تو TypeScript می‌تونیم تایپ‌ها رو با هم اشتراک بگیریم. به‌عنوان مثال فرض کنیم از قبل یه تایپ به اسم Person داریم. حالا می‌خوایم یه تایپ جدید درست کنیم که تمام ویژگی‌های Person رو داشته باشه و بخوایم علاوه بر اون‌ها، username و password رو هم بهش اضافه کنیم:

type User = Person & { username: string; password: string };

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

type User = Person & {
username: string;
password: string;
};

Type Assertions

بعضی مواقع هست که ما به‌عنوان برنامه‌نویس، بیشتر از TypeScript اطلاعات داریم. مثلاً می‌دونیم تایپ یه متغیر خاص قطعاً number ئه.

تو اینجور مواقع می‌تونیم از Assertion استفاده کنیم.

به‌عنوان مثال، این کد رو در نظر بگیرید:

<body>
<h1>Hello, friend!</h1>
<button>Click Me!</button>
</body>
const button = document.querySelector("button");

button.addEventListener("click", () => {
console.log("Clicked...");
});

تایپِ خروجِ document.querySelector برابر با Element | null ئه. اگه بتونه المان رو پیدا کنه Element برمیگردونه؛ اگه نتونه، null برمیگردونه.

اما از اونجایی که ما خودمون کد HTML رو نوشتیم، می‌دونیم که این دکمه وجود داره.

تو چنین جایی ما اطلاعاتمون از TypeScript بیشتره. پس می‌تونیم یکی از این دو کار رو انجام بدیم:

const button = document.querySelector("button")!;
const button = document.querySelector("button") as Element;

مورد اول به مراتب کم‌خطرتره. صرفاً به TypeScript میگه این تایپ نمی‌تونه null باشه.

اما مورد دوم، می‌تونه تایپ رو هر چیزی در نظر بگیره:

const button = document.querySelector("button") as number;

کاملاً واضحه که کد بالا ایراد داره و امکان نداره خروجیِ document.querySelector برابر با number باشه. اما چون از as استفاده کردیم، TypeScript به حرف ما اعتماد می‌کنه و از اینجا به بعد تایپ button رو number در نظر می‌گیره.

اخطار

تقریباً هیچوقت نباید از as استفاده کنید. مگر در موارد خیلی نادر و کاملاً محتاطانه که چاره‌ای جز این کار نداشته باشیم.

Narrowing

به این مثال توجه کنید:

type Action = "increment" | "decrement";

type Params = {
action: Action;
value: number;
};

function perform({ action, value }: Params): number {
if (action === "increment") {
return value + 1;
}

if (action === "decrement") {
return value - 1;
}

return value;
}

const result = perform({
action: "increment",
value: 23,
});

ما اینجا یه تابع داریم که دو تا ورودی می‌گیره. یکی action که مشخص می‌کنه تابع باید چه کاری انجام بده. و یکی value که مقداری که قراره عملیات روش انجام بشه رو به ما میگه.

تا اینجا نکته‌ی خاصی وجود نداره. اما فرض کنید بخوایم یه نوع عملیات دیگه هم اضافه کنیم که احتیاج به مقدار ثانویه‌ای داشته باشه.

مثلاً می‌خوایم عملیات ضرب داشته باشیم که باید یه ضریب هم تو ورودی دریافت کنه:

type Action = "increment" | "decrement" | "multiply";

type Params = {
action: Action;
value: number;
multiplier: number;
};

function perform({ action, value, multiplier }: Params): number {
if (action === "increment") {
return value + 1;
}

if (action === "decrement") {
return value - 1;
}

if (action === "multiply") {
return value * multiplier;
}

return value;
}

const result = perform({
action: "multiply",
value: 23,
multiplier: 2,
});

به نظر میرسه کد درسته ولی یه مشکلی وجود داره. الان کسی که بخواد عملیات increment رو اجرا کنه، مجبوره یه multiplier هم پاس بده، در صورتی که برای این عملیات بهش احتیاج نداریم.

پس می‌تونیم به یکی از روش‌های زیر multiplier رو اختیاری کنیم:

type Params = {
action: Action;
value: number;
multiplier: number | undefined;
};
type Params = {
action: Action;
value: number;
multiplier?: number;
};

اما الان یه مشکل دیگه‌ای به وجود اومد. از کجا معلوم کسی که می‌خواد از multiply استفاده کنه، واقعاً multiplier رو پاس بده؟

برای حل این مشکل باید شرطمون رو آپدیت کنیم:

function perform({ action, value, multiplier }: Params): number {
if (action === "increment") {
return value + 1;
}

if (action === "decrement") {
return value - 1;
}

if (action === "multiply" && multiplier !== undefined) {
return value * multiplier;
}

return value;
}

همونطور که دیدید، TypeScript کاملاً هوشمندانه متوجه شد که وقتی وارد شرط می‌شیم، دیگه امکان نداره multiplier برابر با undefined باشه، بنابراین دیگه به ما خطا نشون نداد.

به این ویژگیِ TypeScript میگن Narrowing.

Discriminated Unions

ما می‌تونیم پا رو فراتر بذاریم و به TypeScript بگیم تایپ فیلدهامون رو بر اساس مقدار یه فیلد خاص در نظر بگیره.

به‌عنوان نمونه تو مثال قبل، فیلدِ action تعیین کننده بود که آیا multiplier داریم یا نه.

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

type Params =
| {
action: "increment" | "decrement";
value: number;
}
| {
action: "multiply";
value: number;
multiplier: number;
};

function perform(params: Params): number {
const { action, value } = params;

if (action === "increment") {
return value + 1;
}

if (action === "decrement") {
return value - 1;
}

if (action === "multiply") {
return value * params.multiplier;
}

return value;
}

معنی این کد اینه که Params در مجموع دو حالت داره:

  1. حالت اول زمانیه که action برابر با increment یا decrement باشه. در این صورت یه فیلدِ دیگه به اسم value هم باید پاس داده بشه.
  2. حالت دوم زمانیه که action برابر با multiply باشه. در این صورت علاوه بر value، باید multiplier هم به صورت اجباری پاس داده بشه.

خوبیش اینه زمانی که دارید از این تابع استفاده می‌کنید، بلافاصله بعد از اینکه action رو تعیین کردید، خودِ IDE باقی فیلدها رو بهتون پیشنهاد میده.

TypeScript-specific Types

یه سری تایپ‌ها هستن که مخصوصِ TypeScriptـن و تو JavaScript وجود نداشتن.

any

هموطنور که از اسمش مشخصه، از این تایپ زمانی استفاده می‌کنیم که بخوایم بگیم متغیرمون می‌تون هر چیزی باشه. این تایپ یه جورایی ما رو برمیگردونه به JavaScript. به این معنا که دیگه Type Safety نخواهیم داشت. به خاطر همین، تا جای ممکن نباید ازش استفاده کرد.

به‌عنوان مثال هیچکدوم از کدهای زیر زمان Compile به ما خطا نمیدن. اما واضحه که اشتباه هستن.

let obj: any = { x: 0 };

obj.foo();
obj();
obj.bar = 100;
obj = "hello";

const n: number = obj;
اخطار

تقریباً هیچوقت نباید از any استفاده کنید. دانشجوها معمولاً به خاطر نقص دانش سراغ any میرن. به این معنی که یه راه بهتری برای حل مشکلشون وجود داشته اما بلد نبودن.

unknown

از این تایپ زمانی استفاده می‌کنیم که نمی‌دونیم تایپ متغیرمون چیه. از خیلی جهات شبیه به any ئه اما خوبیش اینه که استفاده ازش بی‌خطره؛ چون اجازه ندارید هیچ کاری با اون متغیر انجام بدید.

تو مثال زیر، چون نمی‌دونیم تایپ x چیه، بنابراین نمی‌تونیم تابع مورد نظر رو از روش صدا بزنیم:

function doSomething(x: unknown) {
x.doAnotherThing(); // TS2339: Property doAnotherThing does not exist on type unknown
}

تو مثال بعد، چون قبلش تایپ رو چک کردیم، مطمئنیم عدده، بنابراین از ویژگی Narrowing استفاده می‌کنیم و می‌تونیم عملیات‌های ریاضی رو انجام بدیم:

function increment(x: unknown) {
if (typeof x === "number") {
return x + 1;
}

return x;
}

never

این تایپ زمانی استفاده میشه که امکان نداشته باشه چنین چیزی وجود داشته باشه.

مثال زیر رو در نظر داشته باشید:

function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // has type 'never'!
}
}

اینجا x می‌تونه string یا number باشه. تو دو تا شرط اول ما این دو تایپ رو بررسی کردیم. پس اصن امکان نداره به شرط آخر برسیم. بنابراین تایپ x اونجا برابر با never میشه.

void

از این تایپ به‌عنوان خروجی توابع استفاده می‌کنیم. زمانی که تابع هیچ returnـی نداشته باشه، یا اگه داره، مقدار خاصی برنمی‌گردونه، از void استفاده می‌کنیم.

function chiz(): void {
console.log("Doesn't return anything!");
}

function yaroo(): void {
console.log("Doesn't return anything in another way!");
return;
}
یادداشت

این تایپ تو JavaScript هم بود اما کاربردش با چیزی که تو TypeScript داریم متفاوته.

Generic

قبلاً گفتیم میشه آرایه رو به دو روش تعریف کرد:

const freinds: string[] = ["Ross", "Rachel", "Chandler", "Monica", "Joey", "Phoebe"];

const friends2: Array<string> = ["Geller", "Green", "Bing", "Geller", "Tribbiani", "Buffay"];

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

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

صف یا Queue یه ساختمان داده‌ست که دو تا متد اصلی به نام‌های enqueue و dequeue داره. اگه از enqueue استفاده کنیم، می‌تونیم آیتم مورد نظر رو به انتهای صف اضافه کنیم. اگه از dequeue استفاده کردیم، از ابتدای صف یه آیتم برمی‌داریم.

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

پس در مجموع میشه گفت هر کی زودتر بیاد، زودتر هم میره؛ که تو برنامه‌نویسی بهش میگن FIFO که مخفف First-in-first-out ئه.

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

  1. صف خالی است. از دکمه‌ی enqueue برای اضافه‌کردن آیتم جدید استفاده کنید.

پیاده‌سازیش تو TypeScript اینجوریه:

class Queue {
private items: number[];

public enqueue(item: number): void {
this.items.push(item);
}

public dequeue(): number | undefined {
if (this.items.length === 0) {
return undefined;
}

return this.items.splice(0, 1)[0];
}
}
نکته

می‌تونیم با استفاده از private و public مشخص کنیم چه کسی به فیلدهای مورد نظرمون می‌تونه دسترسی داشته باشه.

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

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

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

برای اینکه بتونیم یه کاری کنیم که کلاسمون با هر تایپی بتونه کار کنه، باید از Generic استفاده کنیم:

class Queue<T> {
private items: T[];

public enqueue(item: T): void {
this.items.push(item);
}

public dequeue(): T | undefined {
if (this.items.length === 0) {
return undefined;
}

return this.items.splice(0, 1)[0];
}
}

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

type Person = {
firstName: string;
lastName: string;
};

const queue = new Queue<Person>();

queue.enqueue({ firstName: "Bijan", lastName: "Eisapour" });

Utility Types

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

اینجا چند تا رو به‌عنوان نمونه معرفی می‌کنیم اما تعدادشون خیلی بیشتر از این حرفاست. می‌تونید از سایت TypeScript لیست و نحوه‌ی استفاده از هر کدوم رو مشاهده کنید.

اطلاع

با ترکیب‌کردن Generic و Utility Types میشه تایپ‌های خیلی پیشرفته درست کرد. اما برای این آموزش نیازی نیست خیلی توش عمیق شیم.

Pick

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

type User = {
name: string;
address: string;
username: string;
password: string;
};

type Person = Pick<User, "name" | "address">;
نکته

برای استفاده از Utility Types نیاز نیست importشون کنید؛ چون جزئی از خودِ TypeScript هستن.

Omit

از این تایپ زمانی استفاده می‌کنیم که بخوایم یک یا چند فیلد رو از یه تایپ دیگه حذف کنیم و با باقی فیلدها یه تایپ جدید بسازیم.

type User = {
name: string;
address: string;
username: string;
password: string;
};

type Person = Omit<User, "username" | "password">;

ReturnType

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

function create(user: User): { status: number; message: string } {
// ...
}

type Response = ReturnType<typeof create>;
نکته

اینجا نمی‌تونیم مستقیم از create استفاده کنیم، چون create تایپ نیست. بنابراین باید اول typeofش رو به دست بیاریم بعد به ReturnType پاسش بدیم.

Further Reading

فیچپرهای TypeScript خیلی خیلی بیشتر از چیزیه که اینجا بهش اشاره کردیم.

پیشنهاد می‌کنم از Handbook برای یادگیری TypeScript استفاده کنید.