Mojtaba Pourkhanlar
About meProjectsBlog

  • 👤About me
  • 🧰Projects
  • ✍️Blog

TypeScript Questions


Interfaces Vs Types

// type
type Human = {
  name: string;
  legs: number;
  head: number;
};
// interface
interface Human {
  name: string;
  legs: number;
  head: number;
}
  • هر دو روش برای تعریف تایپ‌ Human با استفاده از Type و Interface درست است.

  • تفاوت کلیدی: Interface فقط می‌تواند شکل آبجکت (object shape) را توصیف کند. اما Type برای تایپ‌های دیگر مانند primitives, unions و tuplesها می‌تواند استفاده شود.

// primitives
type Name = string;
// object
type Male = {
  name: string;
};
type Female = {
  name: string;
};
// union
type HumanSex = Male | Female;
// tuple
type Children = [Female, Male, Female]
;
  • بر خلاف Type، شما برای تعریف تایپ‌های یک آبجکت فقط باید از Interface استفاده کنید.

interface merging Vs type merging

  • خب interface می‌تونه merge شه.
  • اما type نمی‌شه.
interface Human {
  name: string;
}
interface Human {
  legs: number;
}
  • دو اعلام (declaration) بالا، به شکل زیر تغییر می‌تواند باشد:
interface Human {
  name: string;
  legs: number;
}
  • در اینجا Human را می‌توان با یک interface تعریف کرد: ترکیبی از اعضای هر دو اعلام
interface Human {
  name: string;
}
interface Human {
  legs: number;
}
const h: Human = {
  name: "Ohans Emmaniel",
};

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

  • برای نوشتن همچین چیزی با استفاده از Type، باید آن‌ها را بهم متصل کنیم:
type HumanWithName = {
  name: string;
};
type HumanWithLegs = {
  legs: number;
};
type Human = HumanWithName & HumanWithLegs;
const h: Human = {
  name: "gg",
  legs: 5,
};
  • تفاوت جزئی: هردو Type و Interface قابلیت تعمیم (extend) دارند، اما با syntaxهای متفاوت با Interface شما باید از کلمه کلیدی extends استفاده کنید. اما برای Type، شما باید از یک اتصال دهنده استفاده کنید.

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

تعمیم دادن یک Type با Typeای دیگر

type HumanWithName = {
  name: string;
};
type Human = HumanWithName & {
  legs: number;
  eyes: number;
};
  • تعمیم دادن یک Type با Interface
interface HumanWithName {
  name: string;
}
type Human = HumanWithName & {
  legs: number;
  eyes: number;
};
  • تعمیم دادن یک Interface با Interface
interface HumanWithName {
  name: string;
}
interface Human extends HumanWithName {
  legs: number;
  eyes: number;
}
  • تعمیم دادن یک Interface با یک Type
type HumanWithName = {
  name: string;
};
interface Human extends HumanWithName {
  legs: number;
  eyes: number;
}
  • همانطور که در مثال‌های بالا مشاهده می‌کنید، هیچ دلیل منحصر به فردی برای ترجیح دادن یکی به دیگری وجود ندارد. هرچند که syntaxهای آن‌ها متفاوت است.

unknown Vs any

  • خب any یعنی TypeScript: «باشه بابا! هر کاری خواستی بکن.»
  • اما unknown یعنی: «باشه، ولی قبلش ثابت کن چه تایپی هست.»
let x: unknown = "hello";
x.trim(); // خطا می‌دهد
  • unknown امن‌تره.

void Vs never

void :

  • تابع چیزی رو بر نمیگردونه یا این تابع تموم میشه اما نتیجه‌ای نمی‌دهد (یا فقط undefined می‌دهد).
function logMessage(msg: string): void {
  console.log(msg);
}

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

never :

  • این تابع هیچ وقت به انتها نمی‌رسه یا هرگز هیچ مقدار قابل برگشتی تولید نمی‌کنه
function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

این توابع هرگز exit ندارن.

همیشه یا خطا می‌دن یا بی‌نهایت ادامه می‌دن.

چند تفاوت خیلی مهم

void :

  • یعنی تابع کامل میشه اما خروجیش مهم نیست.
  • می‌تونه undefined باشد.
  • تابع void می‌تونه return داشته باشه (مثلاً return undefined).

never :

  • یعنی تابع هرگز کامل نمیشه.
  • حتی undefined هم نیست!
  • تابع never اجازهٔ داشتن return ندارد چون اصلاً نقطهٔ پایانی وجود ندارد.

never در اینجا میدرخشه

type Shape = "circle" | "square";

function handle(shape: Shape) {
  switch (shape) {
    case "circle":
      return "⚪";
    case "square":
      return "⬜";
    default:
      const _exhaustive: never = shape; // Error if new type added
      return _exhaustive;
  }
}

چرا؟

چون اگر بعداً یک shape جدید اضافه کنی، TypeScript می‌گوید:

«داداش! گفتی اینجا باید never باشه، ولی shape مقدار جدید داره!»

این یعنی کوبیدن bug قبل از اینکه bug شما رو بکوبه 😎🔥


Generics

  • برای ساختن فانکشن/کلاس/اینترفیس‌هایی که تایپشون قابل انعطاف باشه.
function wrap<T>(value: T) {
  return { value };
}

const w = wrap<number>(10);

keyof؟

  • کلیدهای یک شیء رو به‌عنوان union تایپ برمی‌گردونه.
type User = { name: string; age: number };
type Keys = keyof User; // "name" | "age"

keyof T Vs T[keyof T]

  • keyof T → لیست اسم کلیدهای یک تایپ
  • T[keyof T] → لیست نوع مقدارهای کلیدهای تایپ

یه جورایی:

  • keyof T = نگاه کردن به اسم کشوهای کمد
  • T[keyof T] = نگاه کردن به چیزهایی که داخل اون کشوهاست!

فرض کن این تایپ رو داریم:

type User = {
  id: number;
  name: string;
  isAdmin: boolean;
};

// خروجی keyof User
type K = keyof User; // "id" | "name" | "isAdmin"

// خروجی User[keyof User]

type V = User[keyof User]
; // string | number | boolean
  • پس یکی اسم‌هاست، یکی نوع محتواها.

Generic in Function

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]
 {
  return obj[key]
;
}
  • خب K extends keyof T یعنی کلید حتما باید یکی از کلیدهای T باشد
  • و T[K] یعنی نوع مقدار آن کلید
const user = { id: 10, name: "Mojtaba", active: true };

const v1 = getProperty(user, "id"); // type: number
const v2 = getProperty(user, "name"); // type: string

Discriminated Union Type

یک نوع Union عادی مثل اینه:

type Shape = Circle | Square | Triangle;

اما مشکلش اینه که TypeScript نمی‌فهمه الان کدومه!

و Discriminated Union این مشکل رو حل می‌کنه با یه ترفند ساده و جذاب:

همه‌ی انواع داخل یونین یک فیلد مشترک دارن که مقدارش دقیقا مشخصه

به این فیلد می‌گیم discriminator.

اینطوری TS می‌تونه به‌صورت خودکار تشخیص بده الان با کدوم نوع طرفه!

یه دسته شکل داریم؛ هرکدوم یک property ثابت به اسم type دارن:

type Circle = {
  type: "circle";
  radius: number;
};

type Square = {
  type: "square";
  size: number;
};

type Triangle = {
  type: "triangle";
  base: number;
  height: number;
};

type Shape = Circle | Square | Triangle;
  • اینجا type همون discriminator ـه.

حالا

function getArea(shape: Shape) {
  switch (shape.type) {
    case "circle":
      return Math.PI * shape.radius ** 2;

    case "square":
      return shape.size ** 2;

    case "triangle":
      return (shape.base * shape.height) / 2;

    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

// به محض اینکه تو switch نوشتی:
case "circle":
  • خب TypeScript اتوماتیک فهمید shape دقیقا نوع Circle ـه و دیگه می‌تونی radius رو با خیال راحت بخونی.

چرا این قابلیت مهمه؟

  • هندل‌کردن چند نوع مختلف با یک فیلد مشترک

  • خطاهای کامپایل‌تایمی (نه رانتایمی!)

  • کمک به auto-complete فوق‌العاده

  • اجازه می‌ده switch شما کاملاً exhaust بشه

اون قسمت _exhaustiveCheck دقیقاً همین کارو می‌کنه؛

اگه یکی از caseها رو جا بندازی، TS بهت اخطار می‌ده.

Modifier ها در TypeScript

خب Modifier ها سطح دسترسی رو مشخص می‌کنن.

Public :

پیش‌فرض.

همه جا قابل دسترس.

class User {
  public name: string;
}

Private : فقط داخل همان کلاس.

class User {
  private password: string;
}

Protected : داخل کلاس و کلاس‌های ارث‌بر.

class Animal {
  protected age: number;
}

Readonly : بعد از مقداردهی تغییر نمی‌کنه.

class User {
  readonly id: number;
}

Static : متعلق به کلاس است نه instance.

class MathUtil {
  static PI = 3.14;
}

const Vs readonly

Const :

  • برای variable استفاده می‌شه.

ReadOnly :

  • برای property استفاده می‌شه تا بعد از مقدار دهی اولیه نتونن تغییر کنن.
interface User {
  readonly id: number;
}