مقدمه‌ای بر TypeScript: ابرمجموعه جاوا اسکریپت برای کدهای مقیاس‌پذیر

فهرست مطالب

مقدمه‌ای بر TypeScript: ابرمجموعه جاوا اسکریپت برای کدهای مقیاس‌پذیر

در دنیای پویای توسعه نرم‌افزار مدرن، جاوا اسکریپت به عنوان زبان اصلی وب و فراتر از آن، در اکوسیستم‌های مختلفی نظیر Node.js برای بک‌اند، فریم‌ورک‌های موبایل هیبریدی و حتی دسکتاپ، جایگاه ویژه‌ای یافته است. با این حال، با رشد و پیچیدگی پروژه‌ها، محدودیت‌های ذاتی جاوا اسکریپت، به ویژه ماهیت پویا و بدون نوع آن، می‌توانستند به چالش‌هایی جدی در زمینه نگهداری، مقیاس‌پذیری و کیفیت کد منجر شوند. اینجاست که TypeScript، ابرمجموعه قدرتمند جاوا اسکریپت که توسط مایکروسافت توسعه یافته و به صورت متن‌باز عرضه شده است، وارد میدان می‌شود تا انقلابی در نحوه ساخت برنامه‌های جاوا اسکریپتی ایجاد کند.

این مقاله به عنوان یک راهنمای جامع و تخصصی، شما را با TypeScript، اصول، مزایا و کاربردهای پیشرفته آن آشنا می‌کند. هدف ما بررسی عمیق ویژگی‌هایی است که TypeScript را به ابزاری ضروری برای توسعه‌دهندگان حرفه‌ای و تیم‌های بزرگ تبدیل کرده است، به گونه‌ای که بتوانید کدهای مقیاس‌پذیرتر، قابل نگهداری‌تر و ایمن‌تر بنویسید. اگر شما یک توسعه‌دهنده جاوا اسکریپت هستید که به دنبال افزایش کیفیت کد، بهبود بهره‌وری و کاهش خطاهای زمان اجرا در پروژه‌های خود هستید، این مقاله دروازه‌ای به سوی درک عمیق TypeScript و استفاده بهینه از قابلیت‌های آن خواهد بود.

چرا TypeScript اهمیت دارد؟ سفری از جاوا اسکریپت به دنیای تایپ‌های ثابت

جاوا اسکریپت، با انعطاف‌پذیری و پویایی فوق‌العاده‌اش، به برنامه‌نویسان امکان می‌دهد تا به سرعت ایده‌ها را به واقعیت تبدیل کنند. اما همین انعطاف‌پذیری، در پروژه‌های بزرگ با چندین توسعه‌دهنده و چرخه‌های عمر طولانی، می‌تواند به یک شمشیر دولبه تبدیل شود. فقدان تایپ‌های ثابت به معنای آن است که بسیاری از خطاهای مرتبط با نوع، تنها در زمان اجرا (runtime) و پس از استقرار برنامه آشکار می‌شوند که کشف، ردیابی و رفع آن‌ها می‌تواند پرهزینه و زمان‌بر باشد. اینجاست که فلسفه وجودی TypeScript نمایان می‌شود.

TypeScript برای حل این مشکلات طراحی شده است. این زبان با افزودن یک سیستم تایپ استاتیک (static type system) به جاوا اسکریپت، امکان شناسایی طیف وسیعی از خطاها را در زمان توسعه (compile time) فراهم می‌آورد. به عبارت دیگر، TypeScript قبل از اینکه کد شما به مرورگر یا محیط Node.js برسد، مشکلات احتمالی را گوشزد می‌کند. این رویکرد به معنای کاهش چشمگیر خطاهای زمان اجرا، افزایش اطمینان‌پذیری کد و بهبود تجربه توسعه‌دهنده است.

تاریخچه مختصر و فلسفه وجودی

TypeScript در سال 2012 توسط مایکروسافت معرفی شد. تیم توسعه آن، به رهبری آندرس هژلزبِرگ (Anders Hejlsberg)، معمار اصلی زبان‌هایی مانند C# و دلفی، با دیدگاهی بلندپروازانه به سراغ جاوا اسکریپت آمدند. آن‌ها به دنبال راهی بودند تا با حفظ قدرت و انعطاف‌پذیری جاوا اسکریپت، ابزارهایی را برای توسعه‌دهندگان فراهم کنند که امکان ساخت برنامه‌های سازمانی و در مقیاس بزرگ را با همان کیفیت و امنیت زبان‌های تایپ‌دار مانند C# یا Java فراهم آورد. TypeScript به عنوان یک ابرمجموعه (Superset) از جاوا اسکریپت، به این معنی است که هر کد جاوا اسکریپت معتبری، یک کد TypeScript معتبر نیز هست. این سازگاری عقب‌رونده، فرآیند مهاجرت از جاوا اسکریپت به TypeScript را بسیار هموار می‌کند.

چالش‌های جاوا اسکریپت در پروژه‌های بزرگ

در یک پروژه جاوا اسکریپت خالص با صدها یا هزاران خط کد، درک ورودی‌ها و خروجی‌های توابع، ساختار اشیاء و رفتار متغیرها می‌تواند به یک چالش تبدیل شود. تغییرات در یک بخش از کد ممکن است به طور ناخواسته بر بخش‌های دیگر تأثیر بگذارد، بدون اینکه ابزارهای توسعه (IDE) یا کامپایلر، هشداری صادر کنند. این امر به ویژه در تیم‌های بزرگ که توسعه‌دهندگان مختلف روی بخش‌های متفاوتی از کدبیس کار می‌کنند، مشکل‌ساز می‌شود. نیاز به مستندسازی دستی، بررسی‌های کد دقیق و تست‌های گسترده برای پوشش خطاهای احتمالی، سربار زیادی را به پروژه تحمیل می‌کند.

مقیاس‌پذیری و نگهداری‌پذیری کد

TypeScript با افزودن سیستم تایپ، به توسعه‌دهندگان امکان می‌دهد تا قراردادهای (contracts) روشنی برای داده‌ها و توابع خود تعریف کنند. این قراردادها به عنوان یک مستندسازی زنده عمل می‌کنند و به سایر توسعه‌دهندگان کمک می‌کنند تا به سرعت نحوه تعامل با بخش‌های مختلف کد را درک کنند. هنگامی که ساختار داده‌ها یا امضای توابع تغییر می‌کند، TypeScript بلافاصله مکان‌هایی را که نیاز به به‌روزرسانی دارند، مشخص می‌کند. این ویژگی به طور چشمگیری به مقیاس‌پذیری (Scalability) پروژه کمک کرده و فرآیند نگهداری (Maintainability) کد را در طولانی‌مدت تسهیل می‌کند. در نتیجه، زمان کمتری صرف رفع باگ‌های مرتبط با نوع می‌شود و زمان بیشتری برای افزودن قابلیت‌های جدید صرف می‌شود.

TypeScript در برابر JavaScript: تقابل تایپ‌های پویا و ثابت

تفاوت بنیادین بین TypeScript و JavaScript در نحوه مدیریت انواع داده نهفته است. جاوا اسکریپت یک زبان پویا (dynamically typed) است، در حالی که TypeScript یک زبان ثابت (statically typed) است. درک این تفاوت، کلید فهم مزایای TypeScript است.

در جاوا اسکریپت، نوع یک متغیر در زمان اجرا (runtime) تعیین می‌شود. شما می‌توانید به یک متغیر ابتدا یک عدد اختصاص دهید، سپس یک رشته و بعد یک شیء، بدون اینکه کامپایلر یا مفسر در زمان توسعه ایرادی بگیرد:

let data = 10; // data is a number
data = "hello"; // data is now a string
data = { value: true }; // data is now an object

این انعطاف‌پذیری گاهی اوقات مفید است، اما در پروژه‌های بزرگ می‌تواند منجر به خطاهای غیرمنتظره شود. برای مثال، اگر انتظار داشته باشید `data` همیشه یک عدد باشد و سعی کنید عملیات ریاضی روی آن انجام دهید در حالی که به طور ناخواسته به یک رشته تبدیل شده باشد، برنامه شما در زمان اجرا با خطا مواجه خواهد شد.

در مقابل، TypeScript به شما امکان می‌دهد تا نوع یک متغیر را در زمان توسعه (compile time) تعریف کنید و آن را برای همیشه ثابت نگه دارید:

let data: number = 10; // data is explicitly a number
// data = "hello"; // TypeScript Error: Type '"hello"' is not assignable to type 'number'.
data = 20; // OK

این ویژگی به TypeScript اجازه می‌دهد تا قبل از اجرای کد، بسیاری از خطاهای مرتبط با نوع را شناسایی و گزارش کند.

مزایای تایپ‌بندی ثابت در TypeScript

شناسایی خطاها در زمان کامپایل (Compile-time Errors)

یکی از بزرگترین مزایای TypeScript، توانایی آن در شناسایی خطاها در زمان توسعه است. قبل از اینکه کد شما به مرورگر یا سرور منتقل شود، کامپایلر TypeScript کد را بررسی می‌کند و هرگونه ناسازگاری نوعی را گزارش می‌دهد. این بدان معناست که به جای کشف باگ‌ها در محیط تولید، آن‌ها را در مراحل اولیه چرخه توسعه پیدا و رفع می‌کنید. این کار هزینه‌های رفع باگ را به شدت کاهش می‌دهد، زیرا هرچه باگ دیرتر شناسایی شود، رفع آن پرهزینه‌تر خواهد بود.

پشتیبانی پیشرفته IDE و Refactoring

سیستم تایپ TypeScript اطلاعات غنی‌ای را برای ابزارهای توسعه یکپارچه (IDEs) فراهم می‌کند. این اطلاعات به IDEها اجازه می‌دهد تا قابلیت‌هایی نظیر تکمیل خودکار (autocompletion)، پیشنهاد پارامتر (parameter hints)، ناوبری کد (code navigation)، بررسی خطاها در لحظه (on-the-fly error checking) و مهم‌تر از همه، رفاکتورینگ ایمن (safe refactoring) را به طور قابل توجهی بهبود بخشند. به عنوان مثال، اگر نام یک تابع را تغییر دهید، IDE شما می‌تواند به طور هوشمند تمام ارجاعات به آن تابع را در سراسر پروژه به‌روزرسانی کند، در حالی که مطمئن باشد هیچ ارجاعی از دست نرفته یا به اشتباه تغییر نکرده است. این قابلیت‌ها به طور چشمگیری بهره‌وری توسعه‌دهندگان را افزایش داده و از بروز خطاهای انسانی جلوگیری می‌کند.

مستندسازی ضمنی کد (Self-documenting Code)

وقتی انواع داده را به صراحت در کد TypeScript خود تعریف می‌کنید، کد شما به طور طبیعی مستند می‌شود. بدون نیاز به کامنت‌های اضافی، می‌توانید به راحتی ورودی‌ها، خروجی‌ها و ساختار داده‌های استفاده شده در یک تابع یا کلاس را درک کنید. این ویژگی به ویژه در پروژه‌های بزرگ با تیم‌های متعدد که نیاز به درک سریع بخش‌های مختلف کد دارند، بسیار ارزشمند است. این “مستندسازی زنده” همیشه با کد همگام است، بر خلاف کامنت‌های دستی که اغلب با تغییرات کد قدیمی می‌شوند.

بهبود همکاری تیمی و درک کد

در یک تیم توسعه، هماهنگی و درک مشترک از کدبیس بسیار حیاتی است. TypeScript با ارائه یک زبان مشترک برای تعریف ساختار داده‌ها و رابط‌ها، ارتباط بین توسعه‌دهندگان را بهبود می‌بخشد. اعضای تیم می‌توانند با اطمینان بیشتری روی کد یکدیگر کار کنند، زیرا سیستم تایپ به آن‌ها کمک می‌کند تا انتظارات و قراردادهای مربوط به هر بخش از کد را به وضوح درک کنند. این امر منجر به کدبیس‌های منسجم‌تر، کمتر مستعد خطا و در نهایت، خروجی با کیفیت بالاتر می‌شود.

طبیعت ابرمجموعه‌ای TypeScript

همانطور که قبلاً ذکر شد، TypeScript یک ابرمجموعه از جاوا اسکریپت است. این به آن معنی است که هر کد جاوا اسکریپت معتبری، یک کد TypeScript معتبر نیز هست. این ویژگی بسیار مهم است زیرا به توسعه‌دهندگان امکان می‌دهد تا به تدریج از جاوا اسکریپت به TypeScript مهاجرت کنند. نیازی نیست کل پروژه را یکباره بازنویسی کنید؛ می‌توانید فایل‌ها را به تدریج به `.ts` تغییر دهید و شروع به افزودن تایپ‌ها کنید. این سازگاری عقب‌رونده، خطر و پیچیدگی فرآیند مهاجرت را به حداقل می‌رساند و آن را به یک انتخاب جذاب برای پروژه‌های موجود تبدیل می‌کند.

مبانی Type System در TypeScript: شناخت انواع داده و کاربردها

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

انواع داده اولیه و پایه

TypeScript همان انواع داده اولیه جاوا اسکریپت را پشتیبانی می‌کند، اما با افزودن قابلیت نوع‌بندی صریح:

  • number: برای اعداد صحیح و اعشاری. تمام اعداد در TypeScript و JavaScript از نوع `number` هستند و تفاوتی بین اعداد صحیح و اعشاری از نظر نوع وجود ندارد.
  • let age: number = 30;
    let price: number = 19.99;
    
  • string: برای رشته‌های متنی.
  • let name: string = "Alice";
    let greeting: string = `Hello, ${name}!`; // Template literals also work
    
  • boolean: برای مقادیر منطقی (true یا false).
  • let isActive: boolean = true;
    let hasPermission: boolean = false;
    
  • null و undefined: این دو نوع، به ترتیب برای مقادیر `null` و `undefined` جاوا اسکریپت استفاده می‌شوند. به طور پیش‌فرض، `null` و `undefined` می‌توانند به هر نوع دیگری اختصاص داده شوند (در حالت `strictNullChecks: false` در `tsconfig.json`). اما با فعال بودن `strictNullChecks`، تنها می‌توانند به خودشان یا به نوع `any` اختصاص یابند.
  • let someValue: number | null = null;
    let anotherValue: string | undefined = undefined;
    
  • symbol: برای مقادیری که یک شناسه منحصر به فرد را نشان می‌دهند، معمولاً از طریق تابع `Symbol()`.
  • let sym: symbol = Symbol("key");
    
  • bigint: برای اعداد صحیح با اندازه دلخواه، که فراتر از حداکثر اندازه امن اعداد در `number` (یعنی `2^53 – 1`) هستند.
  • let largeNumber: bigint = 9007199254740991n;
    

نوع‌بندی صریح (Explicit Typing) و استنتاج نوع (Type Inference)

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

// Type Inference: TypeScript infers 'message' is a string
let message = "Hello, TypeScript!"; 
// message = 123; // Error: Type 'number' is not assignable to type 'string'.

// Explicit Typing: You explicitly declare 'count' as a number
let count: number = 5;

چه زمانی از هر کدام استفاده کنیم؟

  • استنتاج نوع: برای متغیرهایی که در همان خط تعریف و مقداردهی اولیه می‌شوند و نوع آن‌ها واضح است، از استنتاج نوع استفاده کنید. این کار کد را خواناتر و کوتاه‌تر می‌کند.
  • نوع‌بندی صریح: در موارد زیر نوع‌بندی صریح توصیه می‌شود:
    • هنگامی که یک متغیر بدون مقدار اولیه تعریف می‌شود.
    • let userName: string;
      userName = "Bob";
      
    • برای پارامترهای توابع و مقادیر بازگشتی توابع، که اغلب بخش مهمی از “قرارداد” تابع هستند.
    • function add(a: number, b: number): number {
          return a + b;
      }
      
    • هنگامی که نوع استنتاج شده ممکن است آنقدر عمومی باشد که مورد نظر شما نباشد (مثلاً استنتاج `any` یا انواع گسترده).

انواع پیشرفته TypeScript

TypeScript فراتر از انواع اولیه، مجموعه‌ای غنی از انواع پیشرفته را ارائه می‌دهد که برای سناریوهای پیچیده‌تر طراحی شده‌اند:

`any`, `unknown`, `void`, `never`

  • any: یک نوع قدرتمند (و در عین حال خطرناک!) که نشان می‌دهد متغیر می‌تواند هر نوع مقداری داشته باشد. استفاده از `any` اساساً سیستم تایپ TypeScript را برای آن متغیر خاموش می‌کند. باید از آن با احتیاط بسیار زیاد و فقط زمانی استفاده شود که هیچ راه دیگری برای تایپ‌کردن نباشد (مثلاً هنگام تعامل با کتابخانه‌های JavaScript که تعریف نوع ندارند).
  • let dynamicValue: any = 4;
    dynamicValue = "hello";
    dynamicValue = {};
    dynamicValue.foo(); // No compile-time error, potential runtime error
    
  • unknown: مشابه `any`، اما ایمن‌تر. یک متغیر از نوع `unknown` می‌تواند هر نوع مقداری را بپذیرد، اما شما نمی‌توانید عملیاتی روی آن انجام دهید یا مستقیماً آن را به یک نوع خاص نسبت دهید، مگر اینکه ابتدا نوع آن را با استفاده از Type Guard محدود کنید. این ویژگی `unknown` را برای زمانی که نمی‌دانید نوع یک مقدار چیست، اما می‌خواهید با ایمنی کامل با آن کار کنید، ایده‌آل می‌کند.
  • let userInput: unknown;
    userInput = 5;
    userInput = "some text";
    
    // let someString: string = userInput; // Error: Type 'unknown' is not assignable to type 'string'.
    if (typeof userInput === "string") {
        let someString: string = userInput; // OK, inside the if block, userInput is known to be a string
    }
    
  • void: نشان‌دهنده عدم وجود هر نوع مقداری. معمولاً به عنوان نوع بازگشتی توابعی استفاده می‌شود که هیچ مقداری را برنمی‌گردانند.
  • function warnUser(): void {
        console.log("This is a warning message");
    }
    
  • never: نوع `never` نشان‌دهنده مقادیری است که هرگز رخ نمی‌دهند. این نوع معمولاً برای توابعی استفاده می‌شود که هرگز به پایان نمی‌رسند (مثلاً حلقه‌های بی‌نهایت) یا همیشه یک استثنا پرتاب می‌کنند. همچنین در Type Guardsهای جامع که به تمام حالات ممکن رسیدگی شده، از `never` برای اطمینان از کامل بودن استفاده می‌شود.
  • function error(message: string): never {
        throw new Error(message);
    }
    
    function infiniteLoop(): never {
        while (true) {}
    }
    

آرایه‌ها (Arrays) و تاپل‌ها (Tuples)

  • آرایه‌ها: می‌توانند شامل عناصری از یک نوع خاص باشند.
  • let numbers: number[] = [1, 2, 3];
    let names: Array<string> = ["Alice", "Bob"];
    
  • تاپل‌ها: آرایه‌هایی با تعداد مشخصی از عناصر که نوع هر عنصر در یک موقعیت خاص از قبل مشخص شده است. تاپل‌ها برای زمانی مفید هستند که می‌خواهید یک ساختار داده با تعداد ثابت عناصر و انواع مختلف را نمایش دهید.
  • let person: [string, number] = ["John Doe", 30];
    // person = [30, "John Doe"]; // Error: Type 'number' is not assignable to type 'string'.
    

Enumerations (Enums)

Enums (مخفف Enumerations) راهی برای تعریف مجموعه‌ای از ثابت‌های نام‌گذاری شده هستند. آن‌ها کد را خواناتر و کمتر مستعد خطا می‌کنند، به ویژه زمانی که با مجموعه‌ای محدود و ثابت از مقادیر سروکار دارید.

enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right    // 3
}

let userDirection: Direction = Direction.Up;

enum HttpStatus {
    OK = 200,
    NotFound = 404,
    ServerError = 500
}

let status: HttpStatus = HttpStatus.OK;

اتحاد نوع (Union Types) و تقاطع نوع (Intersection Types)

  • Union Types (|): یک متغیر می‌تواند یکی از چندین نوع ممکن را داشته باشد. این برای زمانی مفید است که یک مقدار می‌تواند از چندین فرم معتبر باشد.
  • function printId(id: number | string) {
        if (typeof id === "string") {
            console.log(id.toUpperCase());
        } else {
            console.log(id);
        }
    }
    printId(101);
    printId("202");
    
  • Intersection Types (&): یک نوع جدید ایجاد می‌کند که تمام ویژگی‌های انواع موجود را با هم ترکیب می‌کند. به عبارت دیگر، یک شیء از نوع تقاطع، باید دارای تمام ویژگی‌های هر یک از انواع تشکیل‌دهنده باشد.
  • interface User {
        id: number;
        name: string;
    }
    
    interface Admin {
        privileges: string[];
    }
    
    type AdminUser = User & Admin;
    
    let admin: AdminUser = {
        id: 1,
        name: "SuperAdmin",
        privileges: ["read", "write", "delete"]
    };
    

انواع Literal (Literal Types)

Literal Types به شما امکان می‌دهند تا یک متغیر را به یک مقدار مشخص محدود کنید، نه فقط به یک نوع. این برای سناریوهایی مفید است که شما می‌خواهید ورودی‌ها یا خروجی‌ها به مجموعه‌ای از مقادیر دقیق محدود شوند.

type CardinalDirection = "North" | "East" | "South" | "West";
let direction: CardinalDirection = "North";
// direction = "Up"; // Error: Type '"Up"' is not assignable to type 'CardinalDirection'.

type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
function handleRequest(method: HTTPMethod) {
    // ...
}

سازماندهی کد با Interfaces و Classes: ستون‌های برنامه‌نویسی شیءگرا

TypeScript به شدت از مفاهیم برنامه‌نویسی شیءگرا (OOP) حمایت می‌کند و ابزارهای قدرتمندی برای تعریف ساختارها و رفتارهای شیءگرا ارائه می‌دهد. Interfaces (رابط‌ها) و Classes (کلاس‌ها) دو عنصر کلیدی در این زمینه هستند که به سازماندهی، توسعه و نگهداری کدهای پیچیده کمک شایانی می‌کنند.

رابط‌ها (Interfaces): تعریف شکل اشیاء و قراردادها

یک Interface در TypeScript راهی قدرتمند برای تعریف “شکل” (shape) اشیاء است. این به شما امکان می‌دهد تا خصوصیاتی را که یک شیء باید داشته باشد و انواع آن‌ها را مشخص کنید. این رابط‌ها نه تنها برای اشیاء ساده، بلکه برای تعریف قراردادهای بین کامپوننت‌ها یا بین یک API و مصرف‌کننده آن نیز استفاده می‌شوند.

interface Person {
    firstName: string;
    lastName: string;
    age?: number; // Optional property
    readonly id: string; // Readonly property
    greet?(message: string): void; // Optional method signature
}

function printPersonDetails(person: Person) {
    console.log(`Name: ${person.firstName} ${person.lastName}`);
    if (person.age) {
        console.log(`Age: ${person.age}`);
    }
    console.log(`ID: ${person.id}`);
    if (person.greet) {
        person.greet("Hello!");
    }
}

let user: Person = {
    firstName: "Jane",
    lastName: "Doe",
    id: "user-123",
    age: 25,
    greet: (msg) => console.log(`${msg} from Jane!`)
};

printPersonDetails(user);

// user.id = "new-id"; // Error: Cannot assign to 'id' because it is a read-only property.

Property Signatures (Optional, Readonly)

همانطور که در مثال بالا دیدید، می‌توانید ویژگی‌ها را به صورت اختیاری (با ?) یا فقط خواندنی (با readonly) تعریف کنید. ویژگی‌های اختیاری به این معنی هستند که شیء ممکن است آن ویژگی را داشته باشد یا نداشته باشد، در حالی که ویژگی‌های فقط خواندنی پس از تخصیص اولیه، قابل تغییر نیستند.

Function Types in Interfaces

رابط‌ها می‌توانند امضای توابع را نیز تعریف کنند:

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
    let result = src.search(sub);
    return result > -1;
};

Extending Interfaces (گسترش رابط‌ها)

رابط‌ها می‌توانند از یکدیگر به ارث ببرند، که این قابلیت به شما امکان می‌دهد تا رابط‌های بزرگتر و پیچیده‌تر را از رابط‌های کوچکتر و قابل استفاده مجدد بسازید:

interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square: Square = { color: "blue", sideLength: 10 };

Implementing Interfaces in Classes (پیاده‌سازی رابط‌ها در کلاس‌ها)

کلاس‌ها می‌توانند یک یا چند رابط را پیاده‌سازی کنند. این به معنای آن است که کلاس متعهد می‌شود که تمام ویژگی‌ها و متدهای تعریف شده در آن رابط را داشته باشد:

interface Greetable {
    name: string;
    greet(phrase: string): void;
}

class Greeter implements Greetable {
    name: string;
    constructor(n: string) {
        this.name = n;
    }
    greet(phrase: string) {
        console.log(`${phrase} ${this.name}`);
    }
}

let myGreeter = new Greeter("World");
myGreeter.greet("Hello");

کلاس‌ها (Classes): پیاده‌سازی اصول OOP

TypeScript به طور کامل از سینتکس کلاس‌های ES6 پشتیبانی می‌کند و قابلیت‌های اضافی مانند modifiersهای دسترسی (access modifiers) را اضافه می‌کند. کلاس‌ها بلوک‌های سازنده اساسی برای برنامه‌نویسی شیءگرا هستند و به شما امکان می‌دهند تا اشیائی با حالت (state) و رفتار (behavior) تعریف کنید.

class Animal {
    // Property Declaration Shorthand (Parameter Properties)
    constructor(protected name: string) { // 'protected' is an access modifier
        this.name = name;
    }

    public makeSound(sound: string): void { // 'public' is default
        console.log(`${this.name} makes a ${sound} sound.`);
    }

    private revealSecret(): void { // 'private' access modifier
        console.log("This is a secret.");
    }
}

class Dog extends Animal { // Inheritance
    constructor(name: string, public breed: string) {
        super(name); // Call parent constructor
        this.breed = breed;
    }

    public bark(): void {
        console.log(`${this.name} the ${this.breed} barks!`);
    }
}

let myDog = new Dog("Buddy", "Golden Retriever");
myDog.makeSound("woof"); // Access public method
myDog.bark();
// myDog.revealSecret(); // Error: 'revealSecret' is private.
// console.log(myDog.name); // Error: 'name' is protected and only accessible within the class and its subclasses.

Access Modifiers (`public`, `private`, `protected`)

  • public: اعضای public (هم ویژگی‌ها و هم متدها) در همه جا قابل دسترسی هستند. این حالت پیش‌فرض است اگر هیچ modifierی مشخص نشود.
  • private: اعضای private فقط از درون کلاسی که در آن تعریف شده‌اند قابل دسترسی هستند.
  • protected: اعضای protected از درون کلاسی که در آن تعریف شده‌اند و از درون کلاس‌های مشتق شده (زیرکلاس‌ها) قابل دسترسی هستند.

Inheritance (ارث‌بری)

کلاس‌ها می‌توانند با استفاده از کلمه کلیدی `extends` از کلاس‌های دیگر به ارث ببرند و ویژگی‌ها و متدهای آن‌ها را به ارث ببرند و/یا آن‌ها را override کنند. کلمه کلیدی `super()` برای فراخوانی سازنده کلاس والد استفاده می‌شود.

Abstract Classes و Members

کلاس‌های انتزاعی (Abstract Classes) نمی‌توانند مستقیماً نمونه‌سازی شوند و معمولاً به عنوان کلاس‌های پایه برای ارث‌بری استفاده می‌شوند. آن‌ها می‌توانند متدهای انتزاعی (Abstract Methods) داشته باشند که فقط امضای آن‌ها در کلاس انتزاعی تعریف شده و پیاده‌سازی آن‌ها باید توسط زیرکلاس‌های غیرانتزاعی انجام شود.

abstract class Department {
    constructor(public name: string) {}

    abstract describe(): void; // Abstract method, must be implemented by subclasses

    printName(): void {
        console.log(`Department: ${this.name}`);
    }
}

class AccountingDepartment extends Department {
    constructor() {
        super("Accounting and Auditing");
    }

    describe(): void {
        console.log("This is the Accounting Department.");
    }
}

// let department = new Department("IT"); // Error: Cannot create an instance of an abstract class.
let accounting = new AccountingDepartment();
accounting.printName();
accounting.describe();

Generics: قدرت بازاستفاده‌پذیری کد با حفظ تایپ‌سیفتی

یکی از پیشرفته‌ترین و قدرتمندترین ویژگی‌های TypeScript، Generics (ژنتیک‌ها) هستند. Generics به شما امکان می‌دهند تا کامپوننت‌هایی بنویسید که نه تنها با یک نوع خاص، بلکه با انواع مختلفی از داده‌ها کار کنند، بدون اینکه انعطاف‌پذیری یا تایپ‌سیفتی (type safety) کد را از دست بدهند. آن‌ها به طور خلاصه به شما اجازه می‌دهند تا توابع، کلاس‌ها و رابط‌هایی بنویسید که در زمان نوشتن، نوع دقیق داده‌ای که با آن کار می‌کنند را نمی‌دانند، اما در زمان استفاده، این نوع مشخص می‌شود و TypeScript تضمین می‌کند که همه چیز به درستی تایپ شده است.

مفهوم Generics و ضرورت آن

فرض کنید می‌خواهید تابعی بنویسید که یک آرایه را می‌گیرد و اولین عنصر آن را برمی‌گرداند. در جاوا اسکریپت، این کار ساده است:

function getFirstElement(arr) {
    return arr[0];
}

اما در TypeScript بدون Generics، باید به نوع any متوسل شوید تا بتواند هر نوع آرایه‌ای را قبول کند، که منجر به از دست رفتن اطلاعات نوع می‌شود:

function getFirstElementAny(arr: any[]): any {
    return arr[0];
}

let firstNum = getFirstElementAny([1, 2, 3]); // Type of firstNum is 'any'
let firstStr = getFirstElementAny(["a", "b", "c"]); // Type of firstStr is 'any'

با Generics، می‌توانیم نوع را پارامتری کنیم. این کار با استفاده از یک متغیر نوع (type variable) که به صورت <T> (یا هر حرف دیگر، اما T رایج است) نشان داده می‌شود، انجام می‌شود:

function getFirstElement<T>(arr: T[]): T {
    return arr[0];
}

let firstNum = getFirstElement([1, 2, 3]); // Type of firstNum is 'number' (inferred)
let firstStr = getFirstElement<string>(["a", "b", "c"]); // Type of firstStr is 'string' (explicit)
let firstBool = getFirstElement([true, false]); // Type of firstBool is 'boolean'

در این مثال، T یک متغیر نوع است که نشان‌دهنده نوع عنصر آرایه است. وقتی تابع را فراخوانی می‌کنید، TypeScript نوع T را بر اساس آرگومانی که به تابع می‌دهید، استنتاج می‌کند. این به شما امکان می‌دهد تا کدی بنویسید که هم انعطاف‌پذیر باشد و هم تایپ‌سیفتی را حفظ کند.

توابع Generic

Generics بیشتر در توابع استفاده می‌شوند. مثال بالا یک تابع generic بود. مثال دیگر برای یک تابع هویت (identity function) که ورودی را به همان شکل برمی‌گرداند:

function identity<T>(arg: T): T {
    return arg;
}

let output1 = identity<string>("myString"); // Type: string
let output2 = identity(123); // Type: number (inferred)

کلاس‌های Generic

کلاس‌ها نیز می‌توانند generic باشند، به خصوص برای ساختارهای داده‌ای مانند صف‌ها، پشته‌ها یا درختان که با هر نوع داده‌ای می‌توانند کار کنند:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;

    constructor(zero: T, addFn: (x: T, y: T) => T) {
        this.zeroValue = zero;
        this.add = addFn;
    }
}

let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myGenericNumber.add(10, 20)); // Output: 30

let stringConcatenator = new GenericNumber<string>("", (x, y) => x + y);
console.log(stringConcatenator.add("Hello, ", "World!")); // Output: Hello, World!

رابط‌های Generic

رابط‌ها نیز می‌توانند generic باشند، که برای تعریف شکل اشیائی که با نوع‌های پارامتری‌شده سروکار دارند، بسیار مفید است. به عنوان مثال، یک رابط برای پاسخ‌های API:

interface ApiResponse<T> {
    status: number;
    message: string;
    data: T; // The type of data payload is generic
}

interface UserData {
    id: number;
    name: string;
    email: string;
}

interface ProductData {
    id: number;
    name: string;
    price: number;
}

let userResponse: ApiResponse<UserData> = {
    status: 200,
    message: "Success",
    data: { id: 1, name: "Alice", email: "alice@example.com" }
};

let productResponse: ApiResponse<ProductData> = {
    status: 200,
    message: "Product fetched",
    data: { id: 101, name: "Laptop", price: 1200 }
};

محدودیت‌های Generic (Generic Constraints)

گاهی اوقات، شما می‌خواهید که تابع یا کلاس generic شما با هر نوعی کار نکند، بلکه فقط با انواعی که دارای ویژگی‌های خاصی هستند. این کار با استفاده از Generic Constraints انجام می‌شود، که به شما امکان می‌دهد تا متغیر نوع را به زیرگروهی از انواع محدود کنید. برای مثال، اگر می‌خواهید یک تابع generic بنویسید که طول (length) یک آرگومان را برگرداند، آرگومان باید دارای ویژگی `length` باشد:

interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(arg: T): T {
    console.log(arg.length);
    return arg;
}

logLength("hello"); // Works: string has a 'length' property
logLength([1, 2, 3]); // Works: array has a 'length' property
// logLength(10); // Error: Type 'number' has no 'length' property

با استفاده از extends HasLength، ما به TypeScript می‌گوییم که نوع T باید دارای ویژگی `length` باشد. این به ما امکان می‌دهد تا به `arg.length` به صورت تایپ‌سیف دسترسی پیدا کنیم.

مدیریت ماژول‌ها و تعریف‌های نوع در TypeScript: اکوسیستم گسترده

برای توسعه برنامه‌های مدرن و در مقیاس بزرگ، مدیریت کد در واحدهای کوچک‌تر و قابل استفاده مجدد ضروری است. TypeScript از سیستم ماژول‌های ECMAScript پشتیبانی کامل می‌کند و ابزارهایی برای مدیریت تعریف‌های نوع برای کتابخانه‌های جاوا اسکریپت موجود ارائه می‌دهد.

سیستم ماژول‌ها در TypeScript (ES Modules)

مانند جاوا اسکریپت مدرن (ES Modules)، TypeScript از import و export برای تعریف و استفاده از ماژول‌ها استفاده می‌کند. هر فایل `.ts` به طور پیش‌فرض یک ماژول در نظر گرفته می‌شود اگر دارای دستورات `import` یا `export` باشد.

فایل ./src/utils.ts:

export function add(a: number, b: number): number {
    return a + b;
}

export const PI: number = 3.14;

فایل ./src/app.ts:

import { add, PI } from "./utils";
import { capitalize } from "./string-utils"; // Assume string-utils.ts exists

console.log(add(5, 3)); // 8
console.log(PI); // 3.14
console.log(capitalize("hello")); // Hello

این رویکرد ماژولار به سازماندهی بهتر کد، جلوگیری از تداخل نام‌ها (global namespace pollution) و امکان بارگذاری بهینه کد کمک می‌کند.

فایل‌های تعریف نوع (.d.ts): قلب تعامل با JavaScript

یکی از بزرگترین چالش‌ها هنگام استفاده از TypeScript در یک پروژه موجود جاوا اسکریپت یا هنگام استفاده از کتابخانه‌های جاوا اسکریپت، این است که چگونه TypeScript می‌تواند انواع این کدها را بفهمد. فایل‌های تعریف نوع (Declaration Files) با پسوند `.d.ts` این مشکل را حل می‌کنند. این فایل‌ها شامل هیچ کد اجرایی نیستند، بلکه فقط تعریف‌های نوع را ارائه می‌دهند که TypeScript از آن‌ها برای بررسی نوع کد جاوا اسکریپت استفاده می‌کند.

فرض کنید یک کتابخانه جاوا اسکریپت به نام `my-js-lib.js` دارید:

// my-js-lib.js
function greet(name) {
    return "Hello, " + name;
}

برای استفاده از این تابع در TypeScript با تایپ‌سیفتی، یک فایل تعریف نوع (`my-js-lib.d.ts`) ایجاد می‌کنید:

// my-js-lib.d.ts
declare function greet(name: string): string;

حالا می‌توانید از `greet` در فایل TypeScript خود استفاده کنید و از تایپ‌چکینگ بهره‌مند شوید:

// app.ts
import { greet } from "./my-js-lib";
console.log(greet("TypeScript User"));
// console.log(greet(123)); // Error: Argument of type 'number' is not assignable to parameter of type 'string'.

Ambient Modules (ماژول‌های محیطی)

برای کتابخانه‌هایی که ماژول نیستند (مثلاً متغیرهای سراسری را در پنجره مرورگر قرار می‌دهند) یا برای تعریف‌هایی که باید برای کل پروژه در دسترس باشند، از مفهوم “ماژول‌های محیطی” (Ambient Modules) یا “تعریف‌های سراسری” (Global Declarations) استفاده می‌شود. این‌ها با کلمه کلیدی `declare` شروع می‌شوند.

// globals.d.ts
declare var MY_GLOBAL_VARIABLE: string;
declare function calculateSum(a: number, b: number): number;

Declaration Merging (ادغام تعریف‌ها)

TypeScript از مفهومی به نام “Declaration Merging” پشتیبانی می‌کند. این به این معنی است که اگر چندین تعریف برای یک نام (مثلاً یک Interface) در مکان‌های مختلف داشته باشید، TypeScript آن‌ها را با هم ادغام می‌کند. این ویژگی به ویژه برای گسترش تعریف‌های نوع موجود یا افزودن ویژگی‌ها به آبجکت‌های سراسری (مانند `Window` یا `NodeJS.ProcessEnv`) مفید است.

// Existing interface (e.g., from a library's .d.ts)
interface User {
    id: number;
    name: string;
}

// Your custom extension
interface User {
    email: string;
}

const myUser: User = { id: 1, name: "Test", email: "test@example.com" }; // User now has 'email'

استفاده از DefinitelyTyped و پکیج‌های `@types`

خوشبختانه، نیازی نیست که برای هر کتابخانه جاوا اسکریپت که استفاده می‌کنید، خودتان فایل‌های `.d.ts` بنویسید. پروژه DefinitelyTyped یک مخزن عظیم و جامعه‌محور از فایل‌های تعریف نوع برای هزاران کتابخانه جاوا اسکریپت محبوب (مانند React, Node.js, jQuery, Lodash و غیره) است. این تعریف‌ها به عنوان پکیج‌های npm با پیشوند `@types/` در دسترس هستند.

npm install --save-dev @types/react @types/node @types/lodash

با نصب این پکیج‌ها، TypeScript به طور خودکار تعریف‌های نوع را پیدا کرده و از آن‌ها برای بررسی نوع کد شما استفاده می‌کند، گویی که کتابخانه‌های جاوا اسکریپت از ابتدا در TypeScript نوشته شده‌اند.

تفاوت Namespace و Module

قبل از معرفی ES Modules، TypeScript از مفهوم Namespaces (که قبلاً Internal Modules نامیده می‌شد) برای سازماندهی کد استفاده می‌کرد. Namespaces به شما اجازه می‌دهند تا کد را در یک اسکوپ سراسری اما با نام جداگانه قرار دهید تا از تداخل نام‌ها جلوگیری شود.

// Namespace example
namespace MyUtility {
    export function sum(a: number, b: number): number {
        return a + b;
    }
}

console.log(MyUtility.sum(1, 2));

در حالی که Namespaces هنوز پشتیبانی می‌شوند، ES Modules (که در TypeScript به سادگی “Modules” نامیده می‌شوند) رویکرد مدرن و توصیه شده برای سازماندهی کد هستند. آن‌ها نه تنها قابلیت‌های Namespaces را فراهم می‌کنند، بلکه به استانداردسازی و تعامل بهتر با ابزارهای اکوسیستم جاوا اسکریپت (مانند Webpack یا Rollup) کمک می‌کنند. برای پروژه‌های جدید، همیشه باید از ES Modules (`import`/`export`) به جای Namespaces استفاده کنید.

پیکربندی TypeScript با `tsconfig.json`: کنترل کامل بر فرآیند کامپایل

فایل `tsconfig.json` یک فایل پیکربندی کلیدی در پروژه‌های TypeScript است. این فایل به کامپایلر TypeScript (tsc) می‌گوید که چگونه فایل‌ها را کامپایل کند، کدام فایل‌ها را شامل شود یا حذف کند، و کدام ویژگی‌های زبان را فعال یا غیرفعال کند. درک و پیکربندی صحیح `tsconfig.json` برای هر پروژه TypeScript ضروری است.

یک `tsconfig.json` معمولی به شکل زیر است:

{
    "compilerOptions": {
        "target": "es2020",
        "module": "commonjs",
        "lib": ["es2020", "dom"],
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "outDir": "./dist",
        "rootDir": "./src",
        "baseUrl": "./src",
        "paths": {
            "@utils/*": ["./utils/*"]
        }
    },
    "include": ["src/**/*.ts"],
    "exclude": ["node_modules", "**/*.test.ts"]
}

گزینه‌های اساسی و پرکاربرد (`compilerOptions`)

  • target: تعیین می‌کند که کد جاوا اسکریپت خروجی با کدام نسخه ECMAScript سازگار باشد (مثلاً “es5”, “es2015”, “es2020”). انتخاب نسخه پایین‌تر به معنای پشتیبانی بیشتر از مرورگرهای قدیمی‌تر است، اما ممکن است به polyfillها نیاز داشته باشد.
  • module: تعیین می‌کند که سیستم ماژول‌بندی خروجی چه باشد (مثلاً “commonjs” برای Node.js، “esnext” یا “es2020” برای ES Modules). این گزینه تأثیر زیادی بر نحوه تولید فایل‌های جاوا اسکریپت برای `import`/`export` دارد.
  • lib: مشخص می‌کند که کدام کتابخانه‌های استاندارد زمان اجرا در دسترس هستند (مثلاً “es2020” برای ویژگی‌های جدید ES، “dom” برای APIهای مرورگر).
  • strict: یک پرچم متا که تمام پرچم‌های بررسی دقیق‌تر نوع را فعال می‌کند (مانند `noImplicitAny`, `strictNullChecks`, `strictFunctionTypes` و غیره). فعال کردن `strict: true` به شدت توصیه می‌شود برای نوشتن کد با کیفیت و ایمن.
  • esModuleInterop: اطمینان حاصل می‌کند که `import` و `export` بین ماژول‌های CommonJS و ES Modules به درستی کار می‌کنند. این گزینه برای سازگاری با کتابخانه‌های جاوا اسکریپت قدیمی‌تر حیاتی است.
  • skipLibCheck: از بررسی نوع فایل‌های `.d.ts` در `node_modules` صرف‌نظر می‌کند. این می‌تواند سرعت کامپایل را افزایش دهد و از خطاهای مرتبط با تعریف نوع در کتابخانه‌های شخص ثالث جلوگیری کند، اما باید با احتیاط استفاده شود.
  • forceConsistentCasingInFileNames: تضمین می‌کند که نام فایل‌ها به صورت حساس به حروف کوچک و بزرگ بررسی شوند. این به جلوگیری از مشکلات در سیستم‌عامل‌های غیرحساس به حروف (مانند ویندوز) کمک می‌کند، که می‌تواند منجر به خطاهای `import` در سیستم‌عامل‌های حساس (مانند لینوکس) شود.
  • outDir: مسیر دایرکتوری که فایل‌های جاوا اسکریپت کامپایل شده در آن قرار می‌گیرند.
  • rootDir: مسیر دایرکتوری ریشه حاوی فایل‌های سورس TypeScript. کامپایلر از این برای حفظ ساختار دایرکتوری در `outDir` استفاده می‌کند.
  • baseUrl و paths: این گزینه‌ها برای پیکربندی حل ماژول (module resolution) استفاده می‌شوند. `baseUrl` مسیر پایه را برای ماژول‌های غیرمطلق مشخص می‌کند و `paths` به شما امکان می‌دهد تا aliases (نام‌های مستعار) برای مسیرهای طولانی‌تر ایجاد کنید، که برای سازماندهی پروژه و وارد کردن ماژول‌ها مفید است.
    // In tsconfig.json:
            // "baseUrl": "./src",
            // "paths": { "@components/*": ["./components/*"] }
    
            // In a .ts file:
            import { Button } from "@components/ui/button"; // Instead of "../components/ui/button"
            

حالت Strict و اهمیت آن

فعال کردن "strict": true در `compilerOptions` یکی از مهم‌ترین توصیه‌ها برای هر پروژه TypeScript است. این گزینه تمامی بررسی‌های سخت‌گیرانه نوع را فعال می‌کند که به طور قابل توجهی کیفیت و ایمنی کد را افزایش می‌دهد. برخی از پرچم‌هایی که با `strict: true` فعال می‌شوند عبارتند از:

  • noImplicitAny: اطمینان حاصل می‌کند که هیچ متغیر یا پارامتری به طور ضمنی دارای نوع `any` نشود.
  • strictNullChecks: از دسترسی به ویژگی‌های `null` یا `undefined` جلوگیری می‌کند، مگر اینکه ابتدا بررسی شوند. این ویژگی به طور چشمگیری خطاهای زمان اجرا (مثل “Cannot read property ‘x’ of undefined”) را کاهش می‌دهد.
  • strictFunctionTypes: اطمینان حاصل می‌کند که پارامترهای توابع به طور واریانس (contravariantly) بررسی شوند، که این کار به جلوگیری از باگ‌های ظریف در هنگام انتساب توابع کمک می‌کند.
  • strictPropertyInitialization: تضمین می‌کند که تمام ویژگی‌های غیرانتخابی کلاس در سازنده مقداردهی اولیه شوند.
  • noImplicitReturns: تضمین می‌کند که تمام مسیرهای کد در یک تابع مقداری را برگردانند، اگر تابع دارای نوع بازگشتی باشد.
  • noFallthroughCasesInSwitch: از “fall-through” ناخواسته در دستورات `switch` جلوگیری می‌کند.

یکپارچه‌سازی با ابزارهای Build (Webpack, Vite, Rollup)

در پروژه‌های وب مدرن، TypeScript به ندرت به تنهایی کامپایل می‌شود. معمولاً به عنوان بخشی از یک زنجیره ابزاری (toolchain) بزرگتر، همراه با Bundlerهایی مانند Webpack، Rollup یا Vite استفاده می‌شود. این Bundlerها از پلاگین‌های خاصی (مانند `ts-loader` برای Webpack یا `@rollup/plugin-typescript` برای Rollup) برای تبدیل کد TypeScript به جاوا اسکریپت قابل اجرا استفاده می‌کنند. `tsconfig.json` نقش حیاتی در هدایت این فرآیند ایفا می‌کند و اطمینان می‌دهد که TypeScript به درستی با بقیه ابزارهای ساخت پروژه شما همگام‌سازی شده است.

به عنوان مثال، در Webpack با `ts-loader`، `tsconfig.json` به `ts-loader` می‌گوید که چگونه TypeScript را کامپایل کند. Bundler سپس خروجی جاوا اسکریپت را می‌گیرد و آن را با سایر فایل‌های پروژه (CSS، تصاویر و غیره) بسته‌بندی می‌کند.

بهترین شیوه‌ها و الگوهای طراحی پیشرفته در TypeScript

فراتر از درک مبانی، نوشتن کد TypeScript مؤثر و قابل نگهداری نیازمند پیروی از بهترین شیوه‌ها و استفاده از الگوهای طراحی پیشرفته است. این موارد به شما کمک می‌کنند تا از قدرت کامل سیستم تایپ TypeScript بهره‌مند شوید و کدهای مقاوم‌تر و خواناتری بنویسید.

استفاده از Type Guards و Type Assertions: ایمنی و کنترل جریان نوع

در بسیاری از موارد، TypeScript می‌تواند نوع یک متغیر را به طور خودکار در بلوک‌های شرطی (مثلاً `if/else`) محدود کند. به این فرآیند Type Narrowing (محدود کردن نوع) می‌گویند و روش‌های مختلفی برای انجام آن وجود دارد که به عنوان Type Guards شناخته می‌شوند.

`typeof`, `instanceof`

اینها ساده‌ترین Type Guardها هستند:

function printId(id: number | string) {
    if (typeof id === "string") {
        // Here, 'id' is narrowed to 'string'
        console.log(id.toUpperCase());
    } else {
        // Here, 'id' is narrowed to 'number'
        console.log(id.toFixed(2));
    }
}

class Dog {
    bark() { console.log("Woof!"); }
}
class Cat {
    meow() { console.log("Meow!"); }
}

function petSound(pet: Dog | Cat) {
    if (pet instanceof Dog) {
        // Here, 'pet' is narrowed to 'Dog'
        pet.bark();
    } else {
        // Here, 'pet' is narrowed to 'Cat'
        pet.meow();
    }
}

User-Defined Type Guards

شما می‌توانید Type Guardهای خود را با تعریف توابعی که یک Type Predicate را برمی‌گردانند، ایجاد کنید. Type Predicate به شکل `parameterName is Type` است.

interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function isBird(pet: Bird | Fish): pet is Bird {
    return (pet as Bird).fly !== undefined;
}

function getAnimalAction(pet: Bird | Fish) {
    if (isBird(pet)) {
        pet.fly(); // TypeScript knows 'pet' is a Bird here
    } else {
        pet.swim(); // TypeScript knows 'pet' is a Fish here
    }
}

Type Assertions (`as`) – چه زمانی و چرا با احتیاط؟

Type Assertion (ادعای نوع) زمانی استفاده می‌شود که شما از TypeScript در مورد نوع یک مقدار آگاه‌تر هستید و می‌خواهید آن را به کامپایلر تحمیل کنید. این مانند Type Casting در سایر زبان‌ها است، اما هیچ بررسی زمان اجرا یا تبدیل داده‌ای انجام نمی‌شود. فقط برای کامپایلر است.

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
// Alternative syntax (less common in TSX/React due to JSX tag conflict):
// let strLength: number = (someValue).length;

احتیاط: Type Assertion نباید به عنوان راهی برای دور زدن بررسی‌های نوع TypeScript استفاده شود. استفاده نادرست از آن می‌تواند منجر به خطاهای زمان اجرا شود که TypeScript قرار بود از آن‌ها جلوگیری کند. فقط زمانی از آن استفاده کنید که کاملاً مطمئن هستید که نوع واقعی شیء با ادعای شما مطابقت دارد (مثلاً هنگام کار با DOM API یا یک کتابخانه جاوا اسکریپت بدون تعریف نوع مناسب).

Discriminated Unions: مدیریت انواع مختلف با تایپ‌سیفتی بالا

Discriminated Unions (اتحادیه‌های متمایز) یک الگوی قدرتمند در TypeScript برای کار با مجموعه‌ای از اشیاء است که هر کدام دارای یک ویژگی مشترک (discriminant) هستند که به شما امکان می‌دهد نوع شیء را در زمان اجرا تشخیص دهید.

interface Circle {
    kind: "circle";
    radius: number;
}

interface Square {
    kind: "square";
    sideLength: number;
}

interface Triangle {
    kind: "triangle";
    base: number;
    height: number;
}

type Shape = Circle | Square | Triangle; // Discriminated Union

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.sideLength ** 2;
        case "triangle":
            return 0.5 * shape.base * shape.height;
        default:
            // This line ensures exhaustive checking. If a new shape is added to 'Shape'
            // and not handled here, TypeScript will give a compile-time error.
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck;
    }
}

let myCircle: Circle = { kind: "circle", radius: 5 };
console.log(getArea(myCircle));

استفاده از never در بخش `default` یک ترفند عالی برای اطمینان از کامل بودن (exhaustive checking) است. اگر یک نوع جدید به `Shape` اضافه کنید و آن را در `switch` مدیریت نکنید، TypeScript به شما خطا می‌دهد، که از باگ‌های احتمالی جلوگیری می‌کند.

Conditional Types و Type-Level Programming (مقدمه برای جامعه تخصصی)

TypeScript فراتر از تعریف انواع ساده، قابلیت‌های پیشرفته‌ای برای “برنامه‌نویسی در سطح نوع” (Type-Level Programming) ارائه می‌دهد. Conditional Types (انواع شرطی) و Mapped Types (انواع نگاشت شده) از جمله این قابلیت‌ها هستند که به شما امکان می‌دهند انواع جدیدی را بر اساس روابط بین انواع موجود تعریف کنید.

Conditional Types (`T extends U ? X : Y`)

انواع شرطی به شما امکان می‌دهند تا یک نوع را بر اساس یک شرط انتخاب کنید. این شبیه به یک عملگر سه‌تایی در جاوا اسکریپت است، اما در سطح نوع کار می‌کند.

type IsNumber<T> = T extends number ? "Yes" : "No";

type Result1 = IsNumber<10>;     // "Yes"
type Result2 = IsNumber<"hello">; // "No"

`infer` keyword

کلمه کلیدی `infer` در Conditional Types برای استنتاج یک نوع از درون یک نوع دیگر استفاده می‌شود، که به شما امکان می‌دهد انواع پیچیده‌تری را ایجاد کنید. مثلاً استخراج نوع پارامترهای یک تابع:

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

type Func = (a: number, b: string) => boolean;
type FuncReturnType = GetReturnType<Func>; // boolean

Mapped Types (با مثال ساده)

Mapped Types به شما امکان می‌دهند تا یک نوع جدید را با تکرار روی ویژگی‌های یک نوع موجود ایجاد کنید، و هر ویژگی را به شکلی تغییر دهید. این برای تبدیل یک نوع به دیگری بسیار مفید است (مثلاً همه ویژگی‌ها را اختیاری یا فقط خواندنی کنید).

type MyReadonly<T> = {
    readonly [P in keyof T]: T[P];
};

interface User {
    name: string;
    age: number;
}

type ReadonlyUser = MyReadonly<User>;
// ReadonlyUser will be { readonly name: string; readonly age: number; }

طراحی ایمن در برابر Null و Undefined (Null Safety)

خطاهای مرتبط با `null` و `undefined` (معروف به “Billion-Dollar Mistake”) یکی از رایج‌ترین منابع باگ در جاوا اسکریپت هستند. TypeScript با فعال کردن "strictNullChecks": true در `tsconfig.json`، این مشکل را به طور اساسی حل می‌کند. با این پرچم، `null` و `undefined` دیگر به طور ضمنی قابل انتساب به انواع دیگر نیستند و باید به صراحت در نوع ذکر شوند.

let username: string = "Alice";
// username = null; // Error: Type 'null' is not assignable to type 'string'. (with strictNullChecks)

let optionalName: string | null = null; // OK
optionalName = "Bob"; // OK

برای تعامل ایمن با مقادیری که ممکن است `null` یا `undefined` باشند، TypeScript دو عملگر مفید را ارائه می‌دهد:

  • Optional Chaining (`?.`): برای دسترسی به ویژگی‌ها یا فراخوانی متدها در اشیائی که ممکن است `null` یا `undefined` باشند، بدون پرتاب خطا.
  • interface UserProfile {
        name?: string;
        address?: {
            street?: string;
        };
    }
    
    let user: UserProfile = {};
    console.log(user.address?.street); // undefined (no error)
    
  • Nullish Coalescing (`??`): برای ارائه یک مقدار پیش‌فرض زمانی که یک عبارت `null` یا `undefined` باشد (برخلاف عملگر `||` که برای مقادیر falsy مانند `0` یا `”` نیز فعال می‌شود).
  • let userName: string | null = null;
    let displayName = userName ?? "Guest"; // displayName is "Guest"
    
    let count: number | null = 0;
    let finalCount = count ?? 10; // finalCount is 0 (not 10, because 0 is not nullish)
    

اولویت‌بندی Interface یا Type Alias؟

هم `interface` و هم `type` (Type Alias) می‌توانند برای تعریف شکل اشیاء استفاده شوند:

interface PersonInterface {
    name: string;
    age: number;
}

type PersonType = {
    name: string;
    age: number;
};

تفاوت‌های کلیدی:

  • Declaration Merging: `interface`ها می‌توانند به طور خودکار ادغام شوند، به این معنی که می‌توانید چندین اعلان `interface` با یک نام داشته باشید و TypeScript آن‌ها را به یک `interface` واحد ترکیب می‌کند. `type`ها این قابلیت را ندارند. این ویژگی `interface`ها را برای تعریف‌های نوع پچ شده یا گسترش انواع موجود از کتابخانه‌ها مفید می‌کند.
  • Extensibility: هر دو می‌توانند توسط `extends` یا `&` گسترش یابند.
  • Capabilities: `type`ها می‌توانند برای تعریف هر نوعی (literals, unions, tuples, intersections, mapped types, conditional types) استفاده شوند، در حالی که `interface`ها فقط برای تعریف شکل اشیاء و کلاس‌ها هستند.

توصیه: به طور کلی، برای تعریف شکل اشیاء و کلاس‌ها، interface ترجیح داده می‌شود، به خصوص به دلیل قابلیت Declaration Merging که به پلاگین‌ها و ابزارهای اکوسیستم اجازه می‌دهد تا به راحتی تعریف‌ها را گسترش دهند. اما اگر نیاز به تعریف Union Type، Tuple Type، Literal Type یا استفاده از قابلیت‌های پیشرفته Type-Level Programming (مانند Mapped Types یا Conditional Types) دارید، باید از type استفاده کنید.

نتیجه‌گیری: آینده‌ای با TypeScript برای توسعه‌دهندگان پیشرو

در این مقاله جامع، ما به بررسی عمیق TypeScript، از مبانی آن تا قابلیت‌های پیشرفته و بهترین شیوه‌های کاربردی پرداختیم. دیدیم که چگونه TypeScript با افزودن یک سیستم تایپ استاتیک قوی به جاوا اسکریپت، توانسته است چالش‌های مقیاس‌پذیری و نگهداری‌پذیری کد در پروژه‌های بزرگ را برطرف کند.

مزایای اصلی استفاده از TypeScript عبارتند از:

  • افزایش اطمینان‌پذیری و کاهش باگ‌ها: شناسایی خطاهای نوعی در زمان کامپایل، قبل از رسیدن به زمان اجرا.
  • بهبود بهره‌وری توسعه‌دهنده: پشتیبانی قدرتمند IDE، تکمیل خودکار، Refactoring ایمن و مستندسازی ضمنی کد.
  • کد قابل نگهداری‌تر: تعریف قراردادهای روشن برای داده‌ها و توابع، تسهیل درک کد و همکاری تیمی.
  • پشتیبانی از الگوهای طراحی مدرن: ارائه ابزارهایی مانند Interfaces، Classes و Generics برای ساختارهای شیءگرا و کدهای قابل استفاده مجدد.
  • سازگاری بالا: ابرمجموعه بودن جاوا اسکریپت امکان مهاجرت تدریجی و استفاده از اکوسیستم گسترده جاوا اسکریپت را فراهم می‌کند.

امروزه، TypeScript به یک استاندارد دوفاکتو در بسیاری از حوزه‌های توسعه نرم‌افزار تبدیل شده است. فریم‌ورک‌های بزرگی مانند Angular به طور کامل با TypeScript نوشته شده‌اند، React و Vue.js نیز پشتیبانی قوی از TypeScript را ارائه می‌دهند و در Node.js نیز استفاده از آن به طور فزاینده‌ای محبوب شده است. جوامع فعال، ابزارهای بالغ و پیشرفت‌های مداوم در این زبان، آن را به یک انتخاب عالی برای هر توسعه‌دهنده‌ای که به دنبال ساختن نرم‌افزار با کیفیت بالا و مقیاس‌پذیر است، تبدیل کرده است.

اگر هنوز در پروژه‌های خود از TypeScript استفاده نکرده‌اید، اکنون زمان آن فرا رسیده است که این گام را بردارید. سرمایه‌گذاری در یادگیری و استفاده از TypeScript، نه تنها به بهبود کیفیت کدهای شما کمک می‌کند، بلکه به شما به عنوان یک توسعه‌دهنده، مهارت‌ها و ابزارهایی می‌دهد که در بازار کار مدرن بسیار ارزشمند هستند. شروع کنید، تجربه کنید و قدرت تایپ‌ها را در دستان خود احساس کنید!

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان