Everyday Types

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

The Primitives

سه تایپ string، number و boolean از همه متداول تر اند و در TypeScript و JavaScript یکسان هستند.

Array

برای تعریف آرایه‌ای از هر نوع مقدار استفاده می‌شود. انواع روش‌های تعریف تایپ از نوع آرایه:

// --- syntax 1
// ex: 1. array of numbers
let numbers: number[] = [1, 2, 3];
// ex: 2. array of strings
let names: string[] = ['Ali', 'Reza'];

// --- syntax 2
// ex: 1. array of numbers
let numbers: Array<number> = [1, 2, 3];
// ex: 2. array of strings
let names: Array<string> = ['Ali', 'Reza'];

Any

هرگز پیشنهاد نمی شود از این نوع استفاده کنید زیرا هر نوع داده ای را مجاز می کند و عملاً حضور TypeScript را بی تأثیر می‌کند. این نوع زمانی استفاده می شود که TypeScript نوع داده ای را تشخیص نمی دهد یا نوع داده مدنظر نامشخص است.

const chartConfig: any = {
  title: 'Sales Chart',
  data: [10, 20, 30],
  colors: ['red', 'green', 'blue'],
  animation: true,
};

oldChartLibrary(chartConfig);

اگر نوع متغیر را مشخص نکنید و تایپ اسکریپت هم نتواند نوعش را تخمین (infer) بزند، کامپایلر به‌طور پیش‌فرض آن را any در نظر می‌گیرد. برای جلوگیری از این اتفاق، گزینه زیر را در فایل tsconfig.json خود تنظیم کنید:

tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

Type Annotations on Variables

زمانی که از var، const یا let برای تعریف متغیر استفاده می کنیم، می توانیم یک توضیح نوع به آن اضافه کنیم تا نوع متغیر به صورت کاملاً صریح مشخص شود.

// ex: 1. variable with number type
const age: number = 25;

// ex: 2. variable with string type
const name: string = 'Sara';

// ex: 3. variable with boolean type
const isActive: boolean = true;

// ex: 4
const data: null = null;

// ex: 5
const value: undefined;

Function

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

Parameter Type Annotations

نوع هر پارامتر را می‌توانیم بعد از نام آن مشخص کنیم.

function greetUser(name: string): string {
  return `سلام ${name}! خوش آمدی.`;
}

در این تابع، ورودی name از نوع string تعریف شده. بنابراین تایپ اسکریپت انتظار دارد هنگام استفاده از تابع،آرگومان اول را از نوع string پاس دهیم.

Return Type Annotations

در تایپ اسکریپ می‌توانید نوع مقداری را که یک تابع برمی‌گرداند (return می‌کند)، بعد از پارامتر ها مشخص کنید.

function getFavoriteNumber(): number {
  return 26;
}

در اغلب موارد نیازی به تعریف نوع بازگشتی به صورت دستی نیست. چون خود تایپ اسکریپت به طور نوع خروجی را از روی مقدار return شده تشخیص(infer) می‌دهد.

چرا بعضی از برنامه‌نویسان نوع بازگشتی را می‌نویسند؟

  • برای مستندسازی بهتر کد
  • برای جلوگیری از تغییر ناخواسته‌ی نوع بازگشتی در آینده
  • یا صرفاً به‌عنوان سبک شخصی یا قراردادی در پروژه.

Functions Which Return Promises

مشخص کردن نوع توابعی که خروجی آن‌ها به صورت یک Promise است:

function fetchData(data: string): Promise<string> {
  return new Promise((resolve, reject) => {
    // Simulate a 2-second delay
    setTimeout(() => {
      if (data) {
        resolve(`داده دریافت شد: ${data}`);
      } else {
        reject(new Error('داده ورودی خالی است!'));
      }
    }, 2000);
  });
}

Anonymous Function

TypeScript می تواند نوع ورودی های تابع ناشناس را بسته به موقعیت مورد استفاده تشخیص دهد.

در مثال زیر، TypeScript نوع ورودی name را به دلیل اینکه names آرایه‌ای از رشته‌ها است و متد forEach هم قرار است که تک تک این رشته‌ها را به تابع ناشناس ارسال کند، تشخیص می‌دهد:

const names = ['Alice', 'Bob', 'Eve'];

// ex: 1
// Contextual typing for function - parameter s inferred to have type string
names.forEach((name) => {
  console.log(name.toUpperCase());
});

// ex: 2
// Contextual typing also applies to arrow functions
names.forEach((name) => {
  console.log(name.toUpperCase());
});

Object Types

برای تعریف نوع یک Object کافی است ویژگی‌ها و نوع آنها را فهرست کنید.

در مثال زیر، نوع ورودی pt که یک Object است، در همان زمان تعریف نوشته شده است:

// --- syntax 1
function printCoord(pt: { x: number; y: number }) {
  console.log(`The coordinate's x value is ${pt.x}`);
  console.log(`The coordinate's y value is ${pt.y}`);
}
printCoord({ x: 3, y: 7 });

برای جدا کردن پراپرتی(property) می‌توان از ,(کاما) یا ;(سمی‌کالن) استفاده کرد(قرار دادن جدا کننده اختیاری است).

یکی دیگر از روش های تعیین نوع برای Object، استفاده از interface است.

interface Pt {
  x: number;
  y: number;
}

function printCoord(pt: Pt) {
  console.log(`The coordinate's x value is ${pt.x}`);
  console.log(`The coordinate's y value is ${pt.y}`);
}
printCoord({ x: 3, y: 7 });

Optional Property

برای ایجاد یک پراپرتی اختیاری، باید بعد از نام پراپرتی و قبل از :، علامت ? را قرار دهیم:

function printName(obj: { first: string; last?: string }) {
  // ...
}

// ex: 1
printName({ first: 'Bob' });
// ex: 2
printName({ first: 'Alice', last: 'Alisson' });
function printName(obj: { first: string; last?: string }) {
  console.log(obj.last?.toUpperCase());
}

Union Type

تایپ اسکریپت این امکان را می‌دهد که با استفاده از عملگرها type جدیدی را از ترکیب type های موجود ساخت. یکی از ساده ترین و پرکاربرد ترین روش ها برای ترکیب type ها، union type(نوع ترکیبی) است.

Defining a Union Type

یک Union Type از دو یا چند type دیگر ساخته می‌شود و نشان می‌دهد که یک مقدار می‌تواند یکی از آن type ها باشد. وقتی یک متغیر از نوع union تعریف می‌شود، فقط می‌توان عملیات‌هایی روی آن انجام داد که برای هر یک از اعضای union قابل انجام باشد.

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

function printId(id: number | string) {
  console.log(`Your ID is: ${id}`);
}
// OK
printId(101);
// OK
printId('202');
// Error: Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
printId({ myID: 22342 });

Working with Union

وقتی متغیری از نوع ترکیبی(union) دارید (مثل string | number)، مقدار ورودی می‌تواند هر یک از اعضای union باشد. اما تایپ اسکریپت فقط اجازه‌ی استفاده از عملیات‌هایی را می‌دهد که برای تمام اعضای آن union معتبر باشند.

در مثال زیر تایپ اسکریپت اجازه استفاده از متد toUpperCase را نمی‌دهد چون ممکن است ورودی عدد باشد نه یک رشته:

function printId(id: number | string) {
  console.log(id.toUpperCase());
  // Error: Property 'toUpperCase' does not exist on type 'string | number'.
  //          Property 'toUpperCase' does not exist on type 'number'.
}

برای حل این مشکل، باید نوع را محدود(narrow) کنید؛ یعنی به تایپ اسکریپت نشان دهید در هر بخش از کد، دقیقاً کدام نوع مدنظر شماست.

در مثال زیر، تایپ اسکریپت به‌طور خودکار متوجه می‌شود در شاخه‌ی if مقدار از نوع string و در else از نوع number است.

function printId(id: number | string) {
  if (typeof id === 'string') {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

اگر تمام اعضای یک union دارای ویژگی مشترک باشند، می‌توانید مستقیماً از آن ویژگی استفاده کنید بدون اینکه type ها را محدود(narrow) کنید.

// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
  return x.slice(0, 3);
}

Type Aliases

تا اینجا از type های object و union به صورت مستقیم استفاده کردیم. اما معمولاً نیاز داریم یک نوع را چندین بار استفاده کنیم و بخواهیم آن را با یک نام مشخص فراخوانی کنیم. Type Alias دقیقاً برای همین کار است؛ یعنی یک نام برای هر نوع (Type) تعریف می‌کند.

ساختار آن به شکل زیر است:

interface Point {
  x: number;
  y: number;
}

// Exactly the same as the earlier example
function printCoord(pt: Point) {
  console.log(`The coordinate's x value is ${pt.x}`);
  console.log(`The coordinate's y value is ${pt.y}`);
}

printCoord({ x: 100, y: 100 });

شما می‌توانید از Type Alias نه فقط برای object ها، بلکه برای نام‌گذاری هر نوعی استفاده کنید. برای مثال، می‌توان به یک Union Type نام داد:

type ID = number | string;

نکته: Type Alias نوع جدیدی ایجاد نمی‌کند؛ فقط یک نام جدید برای یک نوع(type) موجود است. کد زیر شاید در نگاه اول غیرمجاز به نظر برسد، اما از نظر TypeScript کاملاً معتبر است، چون هر دو نوع در واقع همان string هستند:

type UserInputSanitizedString = string;

function sanitizeInput(str: string): UserInputSanitizedString {
  return sanitize(str);
}

// Create a sanitized input
let userInput = sanitizeInput(getInput());

// Can still be re-assigned with a string though
userInput = 'new input';

Interfaces

Interface راه دیگری برای نام‌گذاری یک نوع object است

interface Point {
  x: number;
  y: number;
}

function printCoord(pt: Point) {
  console.log(`The coordinate's x value is ${pt.x}`);
  console.log(`The coordinate's y value is ${pt.y}`);
}

printCoord({ x: 100, y: 100 });

همانند مثالی که با type دیدیم، این کد دقیقاً به همان شکل کار می‌کند و یک تایپ از نوع object تعریف می‌کند. تایپ‌اسکریپت فقط به ساختار مقداری که به تابع printCoord ارسال شده اهمیت می‌دهد؛ یعنی فقط بررسی می‌کند که آیا ویژگی‌های مورد انتظار (x و y) وجود دارند یا خیر. به همین دلیل به تایپ‌اسکریپت یک سیستم نوع‌دهی ساختاری (Structural Type System) می‌گویند.

Differences Between Type Aliases and Interfaces

type و interface بسیار شبیه یکدیگر هستند و در بسیاری از موارد می‌توان هر کدام را به جای دیگری استفاده کرد. تقریباً تمام قابلیت‌ های interface در type نیز وجود دارند. مهم ترین تفاوت آنها این است که type را نمی‌توان دوباره باز و ویژگی جدیدی به آن اضافه کرد در حالی که interface همیشه قابل گسترش است.

گسترش پذیری:

  • type
    type Animal = {
      name: string;
    }
    
    type Bear = Animal & {
      honey: boolean;
    }
    
    const bear = getBear();
    bear.name;
    bear.honey;
    
  • interface
    interface Animal {
      name: string;
    }
    
    interface Bear extends Animal {
      honey: boolean;
    }
    
    const bear = getBear();
    bear.name;
    bear.honey;
    

افزودن ویژگی جدید:

  • type: بعد از ایجاد قابل تغییر نیست
    type Window = {
      title: string;
    }
    
    type Window = {
      ts: TypeScriptAPI;
    }
    
    // Error: Duplicate identifier 'Window'.
    
  • interface
    interface Window {
      title: string;
    }
    
    interface Window {
      ts: TypeScriptAPI;
    }
    
    const src = 'const a = "Hello World"';
    window.ts.transpileModule(src, {});
    

در فصل‌های بعدی بیشتر با این مفاهیم آشنا خواهید شد، بنابراین اگر در حال حاضر همه آن‌ها را کاملاً درک نمی‌کنید، نگران نباشید.

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

Type Assertions

گاهی اوقات اطلاعاتی درباره نوع یک مقدار دارید که TypeScript آن را نمی‌داند.

به عنوان مثال، اگر در حال استفاده از document.getElementById باشید، TypeScript فقط می‌داند که این متد نوعی HTMLElement برمی‌گرداند، اما ممکن است شما بدانید که صفحه مدنظرتان دارای یک HTMLCanvasElement با یک ID مشخص است.

در این شرایط، می‌توانید از Type Assertion برای مشخص کردن یک نوع دقیق‌تر استفاده کنید:

const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement;

مانند type annotation، type assertion ها توسط کامپایلر حذف می‌شوند و بر رفتار runtime کد شما تأثیری نخواهند داشت. همچنین می‌توانید از سینتکس angle-bracket (به جز زمانی که کد در یک فایل .tsx باشد) استفاده کنید که معادل است:

const myCanvas = <HTMLCanvasElement>document.getElementById('main_canvas');

TypeScript فقط به Type Assertion هایی اجازه می‌دهد که یک نوع را به نسخه‌ای دقیق‌تر یا عمومی‌تر تبدیل کنند. این قانون از Coercion(اجبار) های "غیرممکن" مانند زیر جلوگیری می‌کند:

const x = "hello" as number;
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. 

گاهی اوقات این قانون ممکن است بیش از حد محتاطانه باشد و Coercion(اجبار) های پیچیده‌تر که ممکن است معتبر باشند را رد کند. اگر چنین اتفاقی افتاد، می‌توانید از دو Type Assertion پشت سر هم استفاده کنید؛ ابتدا به any یا unknown، و سپس به نوع مورد نظر تبدیل کنید:

const a = expr as unknown as T;

Literal Types

علاوه بر type های عمومی string و number، می‌توان در موقعیت‌های type به رشته‌ها و اعداد مشخص اشاره کرد.

یکی از روش‌های درک این موضوع، توجه به این است که JavaScript راه‌های مختلفی برای تعریف یک متغیر ارائه می‌دهد. مقدار متغیر های تعریف شده با کلمات کلیدی var و let قابل تغییر است، اما const چنین امکانی را نمی‌دهد. تایپ اسکریپت از همین قانون برای ساخت literal type ها استفاده می‌کند.

let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
changingString; // let changingString: string
 
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString; // const constantString: "Hello World"

literal type ها تنهایی چندان هم کاربردی نیستند:

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
Type '"howdy"' is not assignable to type '"hello"'. 

داشتن متغیری که فقط بتواند یک مقدار داشته باشد، چندان کاربردی نیست! اما با ترکیب literalها در Union، می‌توانید مفهوم بسیار کاربردی‌تری را بیان کنید؛ برای مثال، تابعی که فقط مجموعه مشخصی از مقادیر شناخته‌شده را می‌پذیرد:

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'. 

Literal Typeهای عددی نیز به همین شکل عمل می‌کنند:

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

البته، می‌توانید این‌ها را با typeهای غیر literal نیز ترکیب کنید:

interface Options {
  width: number;
}
function configure(x: Options | "auto") {
  // ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'. 

یک نوع دیگر از Literal Type نیز وجود دارد: boolean literal. فقط 2 تا تایپ برای literal type از نوع boolean وجود دارد که عبارت‌اند از false و true. خود type boolean در واقع فقط یک نام مستعار (alias) برای Union true | false است.

Literal Inference

وقتی یک متغیر را با یک object مقداردهی اولیه می‌کنید، TypeScript فرض می‌کند که propertyهای آن object ممکن است بعداً تغییر کنند. برای مثال، اگر کدی مانند زیر بنویسید:

const obj = { counter: 0 };
if (someCondition) {
  obj.counter = 1;
}

TypeScript فرض نمی‌کند که انتساب مقدار 1 به propertyای که قبلاً مقدار 0 داشته است، یک خطا باشد. به بیان دیگر، obj.counter باید از نوع number باشد، نه 0، چون نوع داده برای تعیین رفتار خواندن و نوشتن استفاده می‌شود.

همین موضوع برای stringها نیز صدق می‌کند:

declare function handleRequest(url: string, method: "GET" | "POST"): void;
 
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'. 

در مثال بالا، type مربوط به req.method به‌صورت string تشخیص داده می‌شود، نه "GET". دلیل این موضوع این است که ممکن است بین ایجاد req و فراخوانی handleRequest کدی اجرا شود که مقدار req.method را به string دیگری مانند "GUESS" تغییر دهد. بنابراین، TypeScript این کد را دارای خطا در نظر می‌گیرد.

دو روش برای رفع این مشکل وجود دارد.

  1. می‌توانید با اضافه کردن یک type assertion در یکی از دو محل زیر، نتیجه تشخیص type را تغییر دهید:
    // Change 1:
    const req = { url: "https://example.com", method: "GET" as "GET" };
    // Change 2
    handleRequest(req.url, req.method as "GET");
    

    Change 1 به این معناست که req.method همیشه دارای literal type "GET" است در نتیجه انتساب مقادیر دیگر به این property در ادامه امکان‌پذیر نخواهد بود. Change 2 به این معناست که req.method در ورودی تابع دارای مقدار "GET" است
  2. می‌توانید از as const استفاده کنید تا کل object به literal typeها تبدیل شود:
    const req = { url: "https://example.com", method: "GET" } as const;
    handleRequest(req.url, req.method);
    

پسوند as const در سیستم type مشابه const عمل می‌کند و تضمین می‌کند که به‌جای اختصاص یافتن typeهای کلی‌تر مانند string یا number، به تمام propertyها literal type اختصاص داده شود.