مقایسه Type و Interface در تایپ اسکریپت

در این مقاله قصد داریم تا دو مفهوم Type و Interface در تایپ اسکریپت را باهم مقایسه کنیم تا با توجه به ویژگی‌های پروژه‌ای که داریم، بتوانیم بهترین انتخاب را داشته باشیم.

تایپ اسکریپت یک first-class primitive را برای تعریف آبجکت‌هایی که از آبجکت‌های دیگر extend می‌شوند ارائه می‌دهد، یعنی یک interface.

Interfaceها از همان اولین نسخه تایپ اسکریپت وجود داشته‌اند. آن‌ها از برنامه نویسی شی‌گرا (OOP) الهام گرفته شده‌اند و به ما این امکان را می‌دهند تا از ارث‌بری برای ایجاد typeها استفاده کنیم:

interface WithId {
  id: string;
}
 
interface User extends WithId {
  name: string;
}
 
const user: User = {
  id: "123",
  name: "Karl",
  wrongProperty: 123,
Type '{ id: string; name: string; wrongProperty: number; }' is not assignable to type 'User'.
  Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
};

با این حال، آن‌ها با یک جایگزین built-in ارائه می‌شوند، یعنی Type alias که با استفاده از کلمه کلیدی typeتعریف می‌شود. ما در تایپ اسکریپت می‌توانیم از کلمه کلیدی typeبرای نمایش هر نوع تایپی، نه فقط typeهای آبجکت استفاده کنیم.

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

type StringOrNumber = string | number;
 
const func = (arg: StringOrNumber) => {};
 
func("hello");
func(123);
 
func(true);
Argument of type 'boolean' is not assignable to parameter of type 'StringOrNumber'.

مسلماً می‌توانیم از Type alias برای بیان آبجکت‌ها نیز استفاده کنیم. این امر منجر به بحث‌های زیادی در بین کاربران تایپ اسکریپت می‌شود. هنگامی که یک type آبجکت تعریف می‌کنیم، باید از یک interface یا Type alias استفاده کنیم؟

استفاده از Interfaceها برای وراثت آبجکت‌ها

اگر با آبجکت‌هایی کار می‌کنیم که از یکدیگر ارث‌بری می‌کنند، بهتر است از interfaceها استفاده کنیم. مثال ما در بالا، با استفاده از WithId، می‌تواند با Type alias، با استفاده از intersection type بیان شود:

type WithId = {
  id: string;
};
 
type User = WithId & {
  name: string;
};
 
const user: User = {
  id: "123",
  name: "Karl",
  wrongProperty: 123,
Type '{ id: string; name: string; wrongProperty: number; }' is not assignable to type 'User'.
  Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
};

کدی که داریم کاملاً خوب است، اما بهینه نیست. دلیل آن سرعتی است که تایپ اسکریپت می‌تواند تایپ‌های ما را بررسی کند.

هنگامی که ما یک interface را با استفاده از extendsایجاد می‌کنیم، تایپ اسکریپت می‌تواند آن interface را همراه با نام آن در یک رجیستری داخلی کش کند. این به این معنی است که بررسی‌های آینده در برابر آن می‌تواند سریع‌تر انجام شود. در نتیجه اگر یک intersection type که از &استفاده می‌کند را به کار بگیریم، تایپ اسکریپت نمی‌تواند آن را از طریق نام کش کند، یعنی تقریباً هر بار باید آن را مجدداً محاسبه کند.

این کار یک بهینه‌سازی کوچک است، اما اگر از این interfaceها بارها استفاده کنیم باعث ایجاد سربار اضافی می‌شود. به همین دلیل است که TypeScript performance wiki استفاده از interfaceها را برای وراثت آبجکت توصیه می‌کند، ما نیز در این مقاله همین کار را انجام می‌دهیم.

با این حال، استفاده از interfaceها به طور پیش‌فرض توصیه نمی‌شود. اما دلیل این کار چیست؟

موضوع Declaration Merge در Interfaceها

Interfaceها ویژگی دیگری دارند که اگر با آن آشنا نباشیم، می‌تواند بسیار شگفت‌انگیز به نظر برسد.

وقتی دو Interface با نام یکسان در یک scope تعریف می‌شوند آن دو، declarationها خود را باهم ادغام می‌کنند.

interface User {
  name: string;
}
 
interface User {
  id: string;
}
 
const user: User = {
Property 'name' is missing in type '{ id: string; }' but required in type 'User'.
  id: "123",
};

اگر بخواهیم این موضوع را با  typeها امتحان کنیم، کار نخواهد کرد:

type User = {
Duplicate identifier 'User'.
  name: string;
};
 
type User = {
Duplicate identifier 'User'.
  id: string;
};

این رفتار مورد نظر و یک ویژگی زبان ضروری است و برای مدل‌سازی کتابخانه‌های جاوااسکریپت که آبجکت‌های global را تغییر می‌دهند، مانند افزودن متدها به prototypeهای string، استفاده می‌شود.

اما اگر برای این کار آماده نباشیم، می‌تواند منجر به ایجاد باگ‌های گیج‌کننده شود. اگر می‌خواهیم از این امر جلوگیری کنیم، توصیه می‌شود ESLint را به پروژه خود اضافه کنیم و no-redeclare را فعال نماییم.

مقایسه Index Signature در Type و Interface

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

Type alias دارای index signature ضمنی است، اما interfaceها اینطور نیستند. این به این معنی است که آن‌ها به تایپ‌هایی که دارای index signature هستند، قابل انتساب می‌باشند. اما interfaceها این ویژگی را ندارند. این موضوع می‌تواند منجر به خطاهایی مانند خطای زیر شود:

Index signature for type 'string' is missing in type 'x'.
interface KnownAttributes {
  x: number;
  y: number;
}
 
const knownAttributes: KnownAttributes = {
  x: 1,
  y: 2,
};
 
type RecordType = Record<string, number>;
 
const oi: RecordType = knownAttributes;
Type 'KnownAttributes' is not assignable to type 'RecordType'.
  Index signature for type 'string' is missing in type 'KnownAttributes'.

دلیل این خطا این است که یک interface می‌تواند بعداً extend شود. ممکن است ویژگی اضافه شده‌ای داشته باشد که با کلید stringیا مقدار numberمطابقت ندارد.

ما می‌توانیم با اضافه کردن یک index signature صریح به interface خود این مشکل را برطرف کنیم:

interface KnownAttributes {
  x: number;
  y: number;
  [index: string]: unknown; // new!
}

یا به سادگی تغییری ایجاد کنیم که به جای آن از type استفاده نماییم:

type KnownAttributes = {
  x: number;
  y: number;
};
 
const knownAttributes: KnownAttributes = {
  x: 1,
  y: 2,
};
 
type RecordType = Record<string, number>;
 
const oi: RecordType = knownAttributes;

بررسی type و interface از نظر استفاده به عنوان پیش‌فرض

مستندات تایپ اسکریپت یک راهنمای عالی در این مورد دارد که در آن هر یک از ویژگی‌ها، به جز index signature ضمنی را پوشش می‌دهند اما، به نتیجه‌ای متفاوت از نتیجه‌ای که ما در این مقاله به آن رسیدیم می‌رسند.

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

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

ما در این مقاله می‌خواهیم برعکس آن را توصیه کنیم. ویژگی‌های ادغام declarationها و index signature ضمنی به اندازه‌ای شگفت‌انگیز هستند که ما را از استفاده از interfaceها به‌طور پیش‌فرض ترسانده‌اند.

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

جمع‌بندی

ما تا زمانی که به ویژگی خاصی از interfaceها مانند extends نیاز نداریم، باید به طور پیش‌فرض از typeها استفاده کنیم.

  • Interfaceها نمی‌توانند unionها، mapped typeها و یا typeهای شرطی را بیان کنند. اما Type aliasها می‌توانند هر تایپی را به روشنی بیان کنند.
  • Interfaceها می‌توانند از extendsاستفاده کنند، اما typeها نمی‌توانند.
  • وقتی با آبجکت‌هایی کار می‌کنیم که از یکدیگر ارث‌بری می‌کنند، بهتر است از Interfaceها استفاده کنیم. extendsباعث می‌شود تا type checker تایپ اسکریپت کمی سریع‌تر از زمانی که از &استفاده می‌کنیم اجرا شود.
  • Interfaceهایی با همان نام در scope یکسان، declarationهای خود را باهم ادغام می‌کنند، که منجر به اشکالات غیرمنتظره می‌شود.
  • Type aliasها دارای Index Signature ضمنی Record<PropertyKey,known>هستندکه گه‌گاه ظاهر می‌شود.

دیدگاه‌ها:

افزودن دیدگاه جدید