مباحث پایه
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 که تو ساخت تایپهای پیچیده هم کاربرد داره.
کاربرد
اینجا چندتا مثال میزنم از به کار بردن تایپها تو جاهای مختلف پروژه.
let name: string;
function minus(a: number, b: number) {
return a - b;
}
function minus(a, b): number {
return a - b;
}
class Circle {
radius: number;
constructor(radius) {
this.radius = radius;
}
}
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 در مجموع دو حالت داره:
- حالت اول زمانیه که
action
برابر باincrement
یاdecrement
باشه. در این صورت یه فیلدِ دیگه به اسمvalue
هم باید پاس داده بشه. - حالت دوم زمانیه که
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 ئه.
از دموی تعاملی زیر میتونید استفاده کنید تا بهتر براتون جا بیفته:
- صف خالی است. از دکمهی 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 استفاده کنید.