آموزش ساخت و بهکارگیری اینترفیس در Typescript
در این مقاله میخوانید
تایپاسکریپت (TypeScript) یکی از افزونههای جاوااسکریپت است که از محیط اجرای جاوااسکریپت بههمراه Type Checker زمان کامپایل استفاده میکند. TypeScript روشهای متعددی برای استفاده از اشیاء را دراختیارتان قرار میدهد که یکی از این روشها با استفاده از اینترفیس (Interface) است.
در این مقاله، در پلاگین TypeScript یک Interface ایجاد خواهیم کرد و یاد میگیریم که چگونه از اینترفیسها استفاده کنیم و بهسادگی تفاوت بین Interface و Type معمولی را تشخیص دهیم.
کاربردهای اینترفیس در Typescript
- با استفاده از آنها میتوانید قراردادی ایجاد کنید که تمامی کلاسها باید از آن تبعیت کنند.
- در برنامههایتان میتوانید از Typeهای مختلف استفاده کنید.
احتمالاً متوجه شدهاید که هم اینترفیسها و هم تایپها کاربردهای مشابهی دارند. درواقع، بسیاری از کاربران این دو موضوع را باهم اشتباه میگیرند و بهجای یکدیگر از آنها استفاده میکنند. تنها تفاوت موجود این است که در Interfaceها میتوانید بیش از یک تعریف داشته باشید. سپس، خودِ TypeScript این تعاریف را باهم ادغام میکند؛ اما Typeها فقط یک بار میتوانند تعریف شوند. تفاوت دیگر این است که از Type میتوانید بهمنظور معرفی نام مستعار برای انواع دادههای اولیه مثل Boolean و String استفاده کنید.
اینترفیسهای تایپاسکریپت یکی از روشهای کارآمد برای معرفی ساختارهای Type هستند. با استفاده از اینترفیس در Typescript، میتوانید از ساختارهای Type دربرابر تغییر محافظت و همزمان برای آنها مستندسازی کنید. این موضوع باعث خواهد شد که تجربه بهتری از برنامهنویسی کسب کنید.
ایجاد و استفاده از Interface در TypeScript
بهمنظور ایجاد اینترفیس در TypeScript، از کلمه کلیدی Interface بههمراه اسم دلخواه برای Interface استفاده خواهیم کرد و سپس کدهای مدنظر را داخل {} قرار خواهیم داد:
interface Logger { log: (message: string) => void; }
مانند زمانی که از Type استفاده میکنیم، تمامی کدهای مدنظر باید داخل {} قرار بگیرند. در کد بالا، اینترفیس لاگر نشاندهنده شیئی است که عنصری به نام log دارد. درواقع، این عنصر تابعی است که پارامتری از نوع String را دریافت میکند و مقدار void را بازگشت میدهد.
شما میتوانید از اینترفیس Logger مانند هر Type دیگری استفاده کنید. مثال زیر، نمونهای از ایجاد شیئی است که دقیقاً مشابه اینترفیس Logger است:
interface Logger { log: (message: string) => void; } const logger: Logger = { log: (message) => console.log(message), };
مقادیری که در اینترفیس Logger بهجای Type استفاده میشوند، باید همان اعضایی را داشته باشند که هنگام تعریف Interface معرفی شدهاند؛ البته اعضای اختیاری را میتوانید حذف کنید و به استفاده از آنها نیازی نخواهید داشت. همچنین، ازآنجاکه مقادیر دقیقاً باید چیزی باشند که در اینترفیس معرفی شدهاند، اضافهکردن فیلدهای جدید باعث دریافت خطای کامپایل خواهد شد.
در مثال زیر، در قسمت معرفی شیء ویژگی جدیدی اضافه کردهایم که در زمان تعریف Interface وجود نداشت:
interface Logger { log: (message: string) => void; } const logger: Logger = { log: (message) => console.log(message), otherProp: true, };
در این نمونه، شما خطای ۲۳۲۲ را دریافت خواهید کرد؛ زیرا در زمان تعریف اینترفیس در Typescript، مشخصه otherProp تعریف نشده است:
Output Type '{ log: (message: string) => void; otherProp: boolean; }' is not assignable to type 'Logger'. Object literal may only specify known properties, and 'otherProp' does not exist in type 'Logger'. (2322)
مانند زمانی که Type سادهای را تعریف میکنید، با اضافهکردن علامت سؤال به اسم مشخصهها، آنها را میتوانید به مشخصهای اختیاری تبدیل کنید.
افزایش کاربرد Typeها
وقتی Interface تعریف میکنید، از انواع اشیاء مختلف هم میتوانید فراتر بروید و تمامی اطلاعات مربوط به تایپهای گسترشدادهشده را نیز در آنها بگنجانید. با این کار، با استفاده از فیلدهای متداول میتوانید اینترفیسهای کوچکی ایجاد و از آنها برای ایجاد اینترفیسهای جدید استفاده کنید.
تصور کنید که اینترفیسی بهشکل زیر دارید:
interface Clearable { clear: () => void; }
سپس، میتوانید اینترفیس جدیدی بسازید که از اینترفیس قبلی توسعه یافته است و تمامی فیلدهای موجود در اینترفیس قبلی را دارد. در مثال زیر، اینترفیس Logger درواقع از اینترفیس Clearable توسعه یافته است. به خطهای بولدشده دقت کنید:
interface Clearable { clear: () => void; } interface Logger extends Clearable { log: (message: string) => void; }
اینترفیس Logger عضوی به نام Clear دارد که هیچ مقدار جدیدی را قبول نمیکند و مقدار void را برمیگرداند. این عضو جدید از اینترفیس Clearable بهارث برده شده است و کاربرد آن دقیقاً شبیه وقتی است که کد زیر را مینویسیم:
interface Logger { log: (message: string) => void; clear: () => void; }
زمانی که قرار است چندین اینترفیس را با مجموعه فیلدهای مشترک بنویسید، فیلدهای مشترک را میتوانید در اینترفیسی جداگانه وارد کنید و اینترفیسهای بعدی را از همین اینترفیس جدید توسعه دهید.
به مثال اینترفیس Clearable برمیگردیم که در بخش قبلی نوشتیم. تصور کنید برنامه شما به اینترفیس جدیدی مثل StringList نیاز دارد تا ساختار داده حاوی چند رشته مختلف را نمایش دهد:
interface StringList { push: (value: string) => void; get: () => string[]; }
اگر اینترفیس StringList را از اینترفیس Clearable توسعه دهید، مشخص خواهید کرد که این اینترفیس تمامی اعضای اینترفیس Clearable را هم دارد و مشخصه Clear را میتوانید به آن اضافه کنید.
interface StringList extends Clearable { push: (value: string) => void; get: () => string[]; }
اینترفیسها را از انواع اشیاء مثل اینترفیسها و تایپهای معمولی و حتی کلاسهای مختلف میتوانید توسعه دهید.
اینترفیسهای دارای Callable Signature
درصورتیکه اینترفیسی فراخواندنی (Callable) باشد، اطلاعات موجود در بخش تعریف اینترفیس را میتوانید با ایجاد امضای فراخواندنی انتقال دهید. امضاهای فراخواندنی با اضافهکردن تعریف تابع داخل اینترفیس ایجاد میشوند. همچنین، بهجای استفاده از =>، در زمان تنظیم نوع بازگشتی از : استفاده میشود.
برای مثال، قسمت بولدشده کد زیر امضایی فراخواندنی را به اینترفیس Logger اضافه میکند:
interface Logger { (message: string): void; log: (message: string) => void; }
اگر دقت کرده باشید، نحوه تعریف امضاهای فراخواندنی شبیه تعریف توابع ناشناس است؛ اما در زمان تعریف نوع بازگشتی، بهجای استفاده از =>، از علامت : استفاده میکنیم. این یعنی هر مقدار متعلق به اینترفیس لاگر را مستقیماً بهعنوان تابع میتوانید فراخوانی کنید. برای ایجاد مقادیری که با اینترفیس لاگر شما همخوانی داشته باشند، باید به این نیازمندیهای Interface توجه کنید:
- باید فراخواندنی باشد.
- باید مشخصهای به نام log داشته باشد. این مشخصه تابعی است که پارامتری از نوع String را دریافت میکند.
در مثال زیر، متغیری به نام لاگر را معرفی خواهیم کرد که به Type اینترفیس لاگر شما میتواند اختصاص یابد:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message: string) => { console.log(message); } logger.log = (message: string) => { console.log(message); }
برای همخوانی با اینترفیس Logger، مقدار مدنظر باید فراخواندنی باشد؛ بههمیندلیل، متغیر Logger را به باید یک تابع اختصاص دهید:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message: string) => { console.log(message); } logger.log = (message: string) => { console.log(message); }
سپس، مشخصه log را به تابع Logger باید اختصاص دهید:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message: string) => { console.log(message); } logger.log = (message: string) => { console.log(message); }
این موضوع برای اینترفیس Logger ضروری است. افزونبراین، مقادیری که به اینترفیس Logger اختصاص مییابند، باید مشخصهای با عنوان log داشته باشند. این مشخصه نیز تابعی است که رشتهای را بهعنوان پارامتر دریافت میکند و مقدار void را بازگشت میدهد.
درصورتیکه مشخصه log را وارد نکنید، کامپایلر TypeScript ارور ۲۷۴۱ را به شما نمایش میدهد:
Output Property 'log' is missing in type '(message: string) => void' but required in type 'Logger'. (2741)
همچنین، اگر مشخصه log در اینترفیس Logger امضای Type ناسازگاری داشته باشد، پیغام خطای مشابهی دریافت خواهید کرد:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message: string) => { console.log(message); } logger.log = true;
در این نمونه، کامپایلر کد خطای ۲۳۲۲ را نمایش میدهد:
Output Type 'boolean' is not assignable to type '(message: string) => void'. (2322)
یکی از ویژگیهای مثبت این است که متغیرها باید تایپ مشخصی داشته باشند که در مثال بالا میتوانید آنها را مشاهده کنید. در این مثال، متغیر Logger بهگونهای تنظیم شده است که تایپ آن از نوع اینترفیس Logger باشد. با این کار، تایپاسکریپت میتواند نوع توابع Logger و تابع موجود در مشخصه log را بهسادگی تشخیص دهد.
شما این موضوع را میتوانید با حذف اطلاعات مربوط به Type از آرگومانهای هر دو تابع بررسی کنید. اگر دقت کنید، در قسمتهای بولدشده کد زیر، پارامترهای Message هیچ نوع خاصی ندارند:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message) => { console.log(message); } logger.log = (message) => { console.log(message); }
در هر دو نمونه، ویرایشگر میتواند تشخیص دهد که نوع پارامتر String است. درواقع، String همان نوع دادهای محسوب میشود که در اینترفیس Logger موردانتظار است.
اینترفیسهای دارای Index Signature
شما میتوانید به اینترفیسهای خودتان امضای شاخص (Index Signature) اضافه کنید. بدینترتیب، اینترفیسها میتوانند تعداد بینهایتی از مشخصهها را داشته باشند. برای مثال، اگر میخواهید اینترفیس DataRecord ایجاد کنید که تعداد بینهایتی از فیلدهای String داشته باشد، از امضای شاخص بهشکل زیر میتوانید استفاده کنید:
interface DataRecord { [key: string]: string; }
سپس، از اینترفیس DataRecord برای تنظیم تایپ عناصری میتوانید استفاده کنید که چندین پارامتر از نوع String دارند:
interface DataRecord { [key: string]: string; } const data: DataRecord = { fieldA: "valueA", fieldB: "valueB", fieldC: "valueC", // ... };
در این بخش، توانستیم با استفاده از قابلیتهای مختلف TypeScript اینترفیس ایجاد کنیم. همچنین، آموختیم که چگونه از Interfaceها استفاده کنیم. درادامه، با تفاوت Type و Interface بیشتر آشنا خواهیم شد و نحوه ادغام تعاریف و ادغام ماژولها را یاد خواهیم گرفت.
تفاوت Type و Interface
تا اینجا دیدیم که نحوه معرفی Type و Interface کاملاً شبیه بههم است و هر دو پارامترهای مشابهی دارند. برای مثال، اینترفیسی با اسم Logger ایجاد کردیم که از اینترفیس Clearable گسترش یافته بود:
interface Clearable { clear: () => void; } interface Logger extends Clearable { log: (message: string) => void; }
همین کار را میتوانید با استفاده از Type هم تعریف کنیم:
type Clearable = { clear: () => void; } type Logger = Clearable & { log: (message: string) => void; }
همانطورکه در بخش قبلی دیدیم، از نحوه تعریف Type میتوانیم برای معرفی مجموعهای از عناصر از توابع گرفته تا اشیای پیچیده با مجموعهای از عناصر استفاده کنیم. این کار با استفاده از Type نیز ممکن است و Typeها را از Typeهای دیگر میتوانیم گسترش دهیم.
ازآنجاکه نحوه معرفی Type با نحوه معرفی Interface شبیه بههم است، باید به قابلیتهای ویژهای توجه کنید که به هرکدام منحصر هستند و در کد خودتان روش ثابتی را در پیش بگیرید. از یکی از این دو برای معرفی تایپ در کدهای خودتان استفاده کنید و از دیگری فقط وقتی استفاده کنید که میخواهید از قابلیتهای ویژه آن بهره ببرید. بهعنوان مثال، در زمان تعریف Type میتوانید از برخی قابلیتهایی استفاده کنید که در تعریف Interface وجود ندارد. برای نمونه، در تعریف Type قابلیتهای زیر وجود دارد:
- Union types
- Mapped types
- Alias to primitive types
یکی از قابلیتهایی که فقط در زمان تعریف Interface وجود دارد، امکان تلفیق تعاریف است که در بخش بعدی به آن میپردازیم. این ویژگی در مواقعی که میخواهید کتابخانه بنویسید و امکان توسعه تایپهای موجود در کتابخانه را به کاربر بدهید، به شما کمک میکند. این کار در زمان معرفی Type انجامدادنی نیست.
تلفیق تعاریف
TypeScript میتواند چندین تعریف متفاوت را باهم ادغام کند. شما میتوانید چندین تعریف متفاوت برای یک ساختار داده بنویسید و سپس در زمان کامپایل، آنها را با استفاده از کامپایلر تایپاسکریپت باهم ادغام کنید؛ بهگونهای که مانند Type واحد عمل کنند. در این بخش، با نحوه کار این ویژگی و کاربرد آن در زمان استفاده از اینترفیسها آشنا خواهیم شد.
Interfaceهای تایپاسکریپت را میتوانید دوباره بازگشایی کنید و چندین تعریف متفاوت برای اینترفیس واحد داشته باشید. سپس، کامپایلر تعاریف موجود را باهم ادغام خواهد کرد. وقتی میخواهید فیلدهای جدیدی را به اینترفیس قبلی اضافه کنید، این قابلیت به شما کمک میکند.
برای مثال، تصور کنید که اینترفیسی با اسم DatabaseOptions بهشکل زیر دارید:
interface DatabaseOptions { host: string; port: number; user: string; password: string; }
این اینترفیس برای ارسال گزینههای مختلف به دیتابیس کاربرد دارد. کمی بعد، دوباره اینترفیسی با همین نام معرفی میکنید که فیلدی متنی از نوع String با نام dsnUrl دارد:
interface DatabaseOptions { dsnUrl: string; }
زمانی که کامپایلر TypeScript شروع به خواندن کد شما میکند، تمامی تعاریف DatabaseOptions را در تعریفی جامع و واحد تلفیق خواهد کرد. از نگاه کامپایلر TypeScript، اینترفیس DataOptions بهشکل زیر است:
interface DatabaseOptions { host: string; port: number; user: string; password: string; dsnUrl: string; }
درحالحاضر، اینترفیس حاوی تمامی فیلدهایی است که در مرحله اول تعریف کردید و همراه با فیلد dsnUrl است که جداگانه معرفی کردید. اکنون هر دو تعریف باهم تلفیق شدهاند.
Module Augmentation
زمانی که میخواهید مشخصههای جدیدی به ماژولهای فعلی اضافه کنید، تلفیق تعاریف به شما کمک میکند. یکی از کاربردهای Module Augmentation زمانی است که میخواهید فیلدهای بیشتری را به ساختار داده ایجادشده بهواسطه کتابخانه اضافه کنید. این موضوع در کتابخانه Express نودجیاس متداول است و از آن برای ایجاد سرورهای HTTP میتوانید استفاده کنید.
وقتی با Express کار میکنید، یک عنصر درخواست و یک عنصر پاسخ به هندلرهای درخواست شما اختصاص خواهند یافت. عنصر Request معمولاً برای نگهداری دادههای مربوط به درخواستی خاص کاربرد دارد. برای مثال، میتوانید از آن برای نگهداری اطلاعات کاربری استفاده کنید که درخواست HTTP را داده است:
const myRoute = (req: Request, res: Response) => { res.json({ user: req.user }); }
در اینجا، هندلر درخواست فایل json حاوی فیلد User را به کلاینت برگشت میدهد. اطلاعات کاربری که لاگین کرده، با استفاده از میانافزار Express، یعنی مسئول احراز هویت کاربر، به عنصر Request در قسمت دیگری از کد ارسال میشود.
تعریف Type مربوط به اینترفیس Request بهتنهایی فیلد User ندارد؛ بههمیندلیل، قطعه کد بالا باعث بروز ارور ۲۳۳۹ خواهد شد:
Property 'user' does not exist on type 'Request'. (2339)
بهمنظور رفع این مشکل، باید Module Augmentation برای پکیج Express ایجاد و از امکان تلفیق تعاریف برای اضافهکردن مشخصههای جدید به اینترفیس Request استفاده کنید.
درصورتیکه تایپ عنصر Request را در تعریف تایپ Express بررسی کنید، متوجه خواهید شد که اینترفیسی به Global Namespace به نام Express اضافه شده است:
declare global { namespace Express { // These open interfaces may be extended in an application-specific manner via declaration merging. // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts) interface Request {} interface Response {} interface Application {} } }
برای استفاده از Module Augmentation بهمنظور اضافهکردن مشخصه جدید به اینترفیس Request، باید همان ساختار موجود در فایل محلی تعریف Type را رعایت کنید. برای مثال، تصور کنید که فایلی با نام Express.d.ts بهشکل زیر ایجاد و سپس، آن را به گزینه Types فایل tsconfig.json اضافه کردهاید:
import 'express'; declare global { namespace Express { interface Request { user: { name: string; } } } }
از نگاه کامپایلر TypeScript، اینترفیس Request مشخصه User دارد که تایپ آن روی عنصری با اسم Name و از نوع String تعریف شده است. این موضوع بدیندلیل است که تمامی تعاریف مربوط به اینترفیس باهم تلفیق شدهاند.
تصور کنید که قصد دارید کتابخانه ایجاد کنید و این امکان را به کاربران کتابخانه بدهید که Typeهای موجود در آن را گسترش دهند. در این نمونه، از شما خواسته میشود از Interfaceهای موجود در کتابخانه خروجی بگیرید؛ زیرا تعاریف مربوط به Typeهای نرمال از Module Augmentation پشتیبانی نمیکنند.
سخن نهایی
آموزش استفاده از اینترفیس در Typescript؛ در این مطلب آموزشی، چندین اینترفیس در Typescript ایجاد کردیم که نماینده انواع مختلفی از ساختارهای داده هستند و یاد گرفتیم که چگونه میتوانیم از چندین اینترفیس درکنار یکدیگر بهره ببریم تا Typeهای قدرتمندی ایجاد کنیم. همچنین، با تفاوت بین نحوه تعریف Type و Interface آشنا شدیم. حالا میتوانیم برای ساختارهای داده موجود در کدهای خودمان اینترفیسهای مختلفی طراحی کنیم و از آنها نهایت استفاده را ببریم.
سؤالات متداول
۱. TypeScript چیست؟
TypeScript یکی از افزونههای جاوااسکریپت است که از محیط اجرای جاوااسکریپت بههمراه Type Checker زمان کامپایل استفاده میکند.
۲. اینترفیسهای تایپاسکریپت چه کاربردهایی دارند؟
در TypeScript، اینترفیسها دو کاربرد مختلف دارد: ۱. با استفاده از آنها میتوانید قراردادی ایجاد کنید که تمامی کلاسها باید از آن تبعیت کنند؛ ۲. در برنامههای خودتان میتوانید از Typeهای مختلف استفاده کنید.
۳. Interfaceها در تایپاسکریپت چه کاربردی دارند؟
با استفاده از اینترفیس در Typescript، میتوانید از ساختارهای Type دربرابر تغییر محافظت و همزمان برای آنها مستندسازی کنید.