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

فهرست مطالب

مقدمه: چرا سیستم نوع‌بندی تایپ‌اسکریپت؟

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

سیستم نوع‌بندی تایپ‌اسکریپت فراتر از Typeهای ابتدایی مانند string، number و boolean عمل می‌کند. این سیستم، با ارائه قابلیت‌های پیشرفته‌ای نظیر Generic ها، Conditional Types، Mapped Types و غیره، به توسعه‌دهندگان اجازه می‌دهد تا Typeهای بسیار دقیق و انعطاف‌پذیری تعریف کنند که رفتار و ساختار داده‌ها را با جزئیات بی‌نظیری مدل‌سازی می‌کنند. این قابلیت‌ها نه تنها به بهبود کیفیت و پایداری کد کمک می‌کنند، بلکه تجربه توسعه‌دهنده (Developer Experience – DX) را با ارائه تکمیل خودکار هوشمند (IntelliSense)، Refactoring امن‌تر و مستندسازی ضمنی، به طور چشمگیری ارتقا می‌بخشند. درک عمیق این مفاهیم پیشرفته، کلید باز کردن پتانسیل کامل تایپ‌اسکریپت و نوشتن کدهای قوی‌تر، قابل نگهداری‌تر و مقیاس‌پذیرتر است. در این مقاله، ما به بررسی عمیق‌تر این ویژگی‌های قدرتمند می‌پردازیم و نشان می‌دهیم چگونه می‌توان از آن‌ها برای حل چالش‌های پیچیده در طراحی نرم‌افزار استفاده کرد.

هدف ما فراتر از یک معرفی ساده است؛ ما قصد داریم با ارائه مثال‌های عملی و تحلیل دقیق، شما را با ظرافت‌های هر مفهوم آشنا کنیم تا بتوانید از آن‌ها در پروژه‌های خود به بهترین شکل بهره ببرید. از تفاوت‌های ظریف بین Type Aliases و Interfaces گرفته تا قدرت Transformational Mapped Types و انعطاف‌پذیری Conditional Types، هر بخش به دقت مورد بررسی قرار خواهد گرفت تا دید جامعی از توانایی‌های سیستم نوع‌بندی تایپ‌اسکریپت به دست آورید.

مبانی پیشرفته: Type Aliases در مقابل Interfaces و مفهوم Structural Typing

در تایپ‌اسکریپت، دو راه اصلی برای تعریف Typeهای شی‌گرا وجود دارد: Type Aliases و Interfaces. اگرچه در بسیاری از موارد می‌توان از هر دو به جای یکدیگر استفاده کرد، اما تفاوت‌های ظریف و کاربردهای خاصی دارند که درک آن‌ها برای انتخاب صحیح و بهینه‌سازی کد ضروری است.

تفاوت‌های کلیدی و موارد استفاده

Interface یک قرارداد برای شکل شیء تعریف می‌کند. این رویکرد به ویژه برای تعریف ساختار اشیایی که قصد دارید به عنوان پارامتر به توابع ارسال کنید یا از توابع بازگردانید، یا برای تعریف کلاس‌هایی که می‌خواهند یک قرارداد خاص را پیاده‌سازی کنند، مفید است. Interfaces قابلیت Declaration Merging دارند، به این معنی که اگر دو Interface با یک نام در Scope یکسان تعریف شوند، تایپ‌اسکریپت آن‌ها را با هم ادغام می‌کند و یک Interface واحد با تمام Propertyهای هر دو ایجاد می‌کند. این ویژگی برای افزودن Propertyهای جدید به Interfaceهای موجود از کتابخانه‌های خارجی یا برای Modular کردن تعاریف Typeها در فایل‌های مختلف بسیار مفید است.

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

interface User {
  email: string; // Declaration Merging
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

از سوی دیگر، Type Alias یک نام جدید برای هر Type موجود ایجاد می‌کند. این Type می‌تواند یک شیء، یک Union Type، یک Tuple، یک Generic Type یا حتی یک Primitive Type باشد. Type Aliases بسیار انعطاف‌پذیرتر از Interfaces هستند و می‌توانند برای تعریف هر Type دلخواه استفاده شوند. با این حال، Type Aliases از Declaration Merging پشتیبانی نمی‌کنند. اگر دو Type Alias با یک نام تعریف کنید، تایپ‌اسکریپت خطا می‌دهد.

type UserAlias = {
  id: number;
  name: string;
};

type ID = number;
type StringOrNumber = string | number;
type Point = [number, number]; // Tuple Type

const userId: ID = 123;
const coord: Point = [10, 20];

انتخاب بین Interface و Type Alias اغلب به نوع Type و هدف شما بستگی دارد. به طور کلی، برای تعریف شکل اشیاء و کلاس‌ها، Interfaces ترجیح داده می‌شوند، به خصوص اگر نیاز به Declaration Merging داشته باشید یا قصد داشته باشید که Type شما توسط کلاس‌ها پیاده‌سازی شود (با کلمه کلیدی implements). برای Typeهای پیچیده‌تر مانند Union Types، Tuple Types یا برای ایجاد نام‌های مستعار برای Typeهای موجود، Type Aliases انتخاب بهتری هستند. بسیاری از توسعه‌دهندگان از یک قانون سرانگشتی پیروی می‌کنند: اگر شیء است، از Interface استفاده کنید؛ در غیر این صورت، از Type Alias استفاده کنید.

مفهوم Structural Typing و پیامدهای آن

تایپ‌اسکریپت از یک سیستم نوع‌بندی ساختاری (Structural Typing) استفاده می‌کند، که گاهی اوقات “duck typing” نیز نامیده می‌شود (“If it walks like a duck and quacks like a duck, then it is a duck”). این بدان معناست که سازگاری Typeها بر اساس ساختار آن‌ها تعیین می‌شود، نه بر اساس نام آن‌ها. اگر یک Type A تمام Propertyهای یک Type B را داشته باشد، پس A با B سازگار است، حتی اگر هیچ رابطه ارث‌بری صریحی بین آن‌ها وجود نداشته باشد یا Typeها نام‌های متفاوتی داشته باشند.

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

interface Point3D {
  x: number;
  y: number;
  z: number;
}

const point2D: Point2D = { x: 10, y: 20 };
const point3D: Point3D = { x: 10, y: 20, z: 30 };

function logPoint(p: Point2D) {
  console.log(`x: ${p.x}, y: ${p.y}`);
}

logPoint(point2D); // OK
logPoint(point3D); // OK! Point3D has x and y, so it's structurally compatible with Point2D.

این ویژگی قدرت زیادی به تایپ‌اسکریپت می‌دهد، زیرا امکان کدنویسی انعطاف‌پذیرتر و Modular تر را فراهم می‌کند. شما نیازی ندارید که به صراحت یک Interface را پیاده‌سازی کنید تا با آن سازگار باشید؛ فقط کافی است ساختار صحیحی داشته باشید. با این حال، این ویژگی می‌تواند منجر به خطاهای غیرمنتظره‌ای شود اگر انتظار نوع‌بندی اسمی (Nominal Typing) را داشته باشید (که در آن سازگاری بر اساس نام Type تعیین می‌شود، نه ساختار آن). برای مثال، اگر دو Interface با ساختار یکسان اما معنای متفاوت داشته باشید، تایپ‌اسکریپت آن‌ها را سازگار می‌داند.

interface Product {
  name: string;
  price: number;
}

interface Service {
  name: string;
  price: number;
}

const laptop: Product = { name: "MacBook", price: 2000 };
const consulting: Service = { name: "SEO Consultation", price: 500 };

function calculateTax(item: Product) {
  return item.price * 0.10;
}

console.log(calculateTax(consulting)); // OK, even though 'consulting' is a Service.

در این مثال، Service به طور ساختاری با Product سازگار است، حتی اگر از نظر مفهومی متفاوت باشند. برای جلوگیری از چنین ابهاماتی، می‌توانید از تکنیک‌های نوع‌بندی اسمی‌مانند استفاده از “برندهای” منحصر به فرد (Unique Brand Properties) در Typeها استفاده کنید:

interface Product {
  _brand: 'Product'; // Unique brand
  name: string;
  price: number;
}

interface Service {
  _brand: 'Service'; // Unique brand
  name: string;
  price: number;
}

const laptop: Product = { _brand: 'Product', name: "MacBook", price: 2000 };
const consulting: Service = { _brand: 'Service', name: "SEO Consultation", price: 500 };

function calculateTaxStrict(item: Product) {
  return item.price * 0.10;
}

// console.log(calculateTaxStrict(consulting)); // Error: Property '_brand' is missing in type 'Service' but required in type 'Product'.

این رویکرد به شما کمک می‌کند تا کنترل بیشتری بر سازگاری Typeها داشته باشید، به ویژه در سناریوهایی که معنای Type اهمیت بیشتری از ساختار صرف آن دارد.

همچنین، موارد readonly و Optional Properties (?) نقش مهمی در تعریف دقیق‌تر Typeها ایفا می‌کنند. استفاده از readonly برای Propertyهایی که نباید پس از مقداردهی اولیه تغییر کنند، ایمنی کد را افزایش می‌دهد. Optional Properties نیز برای تعریف Propertyهایی که ممکن است در یک شیء وجود داشته باشند یا نداشته باشند، استفاده می‌شوند و انعطاف‌پذیری بیشتری در مدل‌سازی داده‌ها فراهم می‌کنند.

interface Config {
  readonly id: string;
  port?: number; // Optional property
  debug: boolean;
}

const myConfig: Config = {
  id: "abc-123",
  debug: true
};

// myConfig.id = "def-456"; // Error: Cannot assign to 'id' because it is a read-only property.
console.log(myConfig.port); // undefined, but no error.

قدرت Generic ها: Type‌های پارامتری برای قابلیت استفاده مجدد

Generic ها یکی از قدرتمندترین ویژگی‌های سیستم نوع‌بندی تایپ‌اسکریپت هستند که به شما امکان می‌دهند اجزای قابل استفاده مجدد ایجاد کنید که قادر به کار با انواع مختلفی از داده‌ها هستند، بدون اینکه ایمنی Type را از دست بدهند. آن‌ها به شما اجازه می‌دهند Typeها را به عنوان پارامتر به توابع، کلاس‌ها و Interfaces ارسال کنید، درست مانند اینکه توابع مقادیر را به عنوان پارامتر دریافت می‌کنند.

مقدمه‌ای بر Generic ها و نیاز به آن‌ها

تصور کنید یک تابع دارید که یک آرایه را دریافت می‌کند و اولین عنصر آن را برمی‌گرداند. بدون Generic ها، ممکن است مجبور شوید از any استفاده کنید، که اطلاعات Type را از بین می‌برد:

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

const str = getFirstElementAny(["hello", "world"]); // str is 'any'
const num = getFirstElementAny([1, 2, 3]); // num is 'any'
// No Type safety if we then try to use `str` as a number.

یا برای هر Type خاص یک تابع جداگانه بنویسید، که منجر به تکرار کد می‌شود:

function getFirstString(arr: string[]): string {
  return arr[0];
}

function getFirstNumber(arr: number[]): number {
  return arr[0];
}

اینجاست که Generic ها وارد می‌شوند. با Generic ها، می‌توانید یک تابع واحد بنویسید که برای هر Typeی کار کند و در عین حال اطلاعات Type را حفظ کند:

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

const str = getFirstElement(["hello", "world"]); // str is inferred as string
const num = getFirstElement([1, 2, 3]); // num is inferred as number
const bool = getFirstElement([true, false]); // bool is inferred as boolean

<T> یک پارامتر Type است که به آن “Type Variable” می‌گویند. وقتی getFirstElement را فراخوانی می‌کنید، تایپ‌اسکریپت T را بر اساس آرگومان‌های ارسالی استنتاج می‌کند. این قابلیت استفاده مجدد را بدون قربانی کردن ایمنی Type فراهم می‌کند.

extends constraint در Generic ها

گاهی اوقات شما نیاز دارید که Type پارامتر Generic خود را محدود کنید. به عنوان مثال، ممکن است بخواهید تضمین کنید که Type T دارای Property خاصی است. برای این کار از کلمه کلیدی extends استفاده می‌شود.

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // Now we know 'arg' has a .length property
  return arg;
}

loggingIdentity("hello"); // OK, string has a length property
loggingIdentity([1, 2, 3]); // OK, array has a length property
// loggingIdentity(3); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

این محدودیت‌ها (Constraints) به شما اجازه می‌دهند تا عملیات‌های خاصی را بر روی پارامترهای Generic انجام دهید، در حالی که Type Safety را حفظ می‌کنید. Generic ها نه تنها برای توابع، بلکه برای Interfaces، کلاس‌ها و Type Aliases نیز استفاده می‌شوند.

Generic Interfaces, Generic Classes, Generic Functions

Generic ها می‌توانند در تمام بخش‌های Type System تایپ‌اسکریپت به کار روند:

  • Generic Functions: همانطور که در مثال getFirstElement دیدیم، توابع Generic رایج‌ترین کاربرد Generic ها هستند.
  • function identity<T>(arg: T): T {
      return arg;
    }
  • Generic Interfaces: می‌توانید Interfaces را Generic تعریف کنید تا بتوانند با Typeهای مختلفی کار کنند.
  • interface GenericIdentityFn<T> {
        (arg: T): T;
      }
    
      function identity<T>(arg: T): T {
        return arg;
      }
    
      let myIdentity: GenericIdentityFn<number> = identity;
      console.log(myIdentity(10)); // 10, Type is number
  • Generic Classes: کلاس‌ها نیز می‌توانند Generic باشند. این به ویژه برای ساخت ساختارهای داده‌ای مانند Stack، Queue، Tree و غیره مفید است.
  • 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;
        }
    
        sum(arr: T[]): T {
            return arr.reduce((acc, curr) => this.add(acc, curr), this.zeroValue);
        }
      }
    
      let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
      console.log(myGenericNumber.sum([1, 2, 3])); // 6
    
      let stringConcatenator = new GenericNumber<string>("", (x, y) => x + y);
      console.log(stringConcatenator.sum(["hello", " ", "world"])); // "hello world"

keyof و typeof در ترکیب با Generic ها

keyof یک Type Operator است که یک Type از تمام Property Nameهای یک Type شیء را به عنوان Literal String Union Type برمی‌گرداند. این بسیار مفید است وقتی با Generic ها ترکیب می‌شود تا بتوانیم به صورت ایمن به Propertyهای یک شیء دسترسی پیدا کنیم.

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const person = { name: "Alice", age: 30 };
let personName = getProperty(person, "name"); // personName is inferred as string
let personAge = getProperty(person, "age"); // personAge is inferred as number
// let invalidProp = getProperty(person, "address"); // Error: Argument of type '"address"' is not assignable to parameter of type '"name" | "age"'.

typeof Type Operator یک Type از یک متغیر یا Property را برمی‌گرداند. ترکیب آن با keyof می‌تواند برای ساخت Typeهای پویا بسیار قدرتمند باشد.

const COLORS = {
  red: "#FF0000",
  green: "#00FF00",
  blue: "#0000FF",
} as const; // 'as const' makes properties readonly literal types.

type ColorName = keyof typeof COLORS; // "red" | "green" | "blue"
type ColorValue = typeof COLORS[keyof typeof COLORS]; // "#FF0000" | "#00FF00" | "#0000FF"

function getColorHex(colorName: ColorName): ColorValue {
  return COLORS[colorName];
}

console.log(getColorHex("red")); // "#FF0000"
// console.log(getColorHex("yellow")); // Error: Argument of type '"yellow"' is not assignable to parameter of type '"red" | "green" | "blue"'.

Generic ها قلب Type System پیشرفته تایپ‌اسکریپت هستند و درک عمیق آن‌ها برای نوشتن کدهای قابل استفاده مجدد، امن و منعطف ضروری است. آن‌ها پایه و اساس بسیاری از Utility Typeها و الگوهای پیشرفته دیگر را تشکیل می‌دهند که در ادامه به آن‌ها خواهیم پرداخت.

پیمایش پیچیدگی: Conditional Types و قابلیت‌های استخراج نوع (Infer)

Conditional Types (Typeهای شرطی) یکی از پیچیده‌ترین و در عین حال قدرتمندترین ویژگی‌های سیستم نوع‌بندی تایپ‌اسکریپت هستند. آن‌ها به شما اجازه می‌دهند که Type یک Type را بر اساس یک شرط تعیین کنید، که اغلب شامل یک Type Check است. این قابلیت، سطح بی‌سابقه‌ای از انعطاف‌پذیری و Expressiveness را در تعریف Typeهای پویا و وابسته به داده‌ها فراهم می‌آورد.

سینتکس و منطق T extends U ? X : Y

سینتکس یک Conditional Type شبیه به یک Ternary Operator در جاوااسکریپت است: SomeType extends OtherType ? TrueType : FalseType.

  • SomeType extends OtherType: این یک Type Check است که بررسی می‌کند آیا SomeType قابل انتساب به OtherType است یا خیر.
  • TrueType: اگر شرط True باشد، این Type انتخاب می‌شود.
  • FalseType: اگر شرط False باشد، این Type انتخاب می‌شود.
type IsString<T> = T extends string ? "Yes" : "No";

type A = IsString<string>;   // Type A is "Yes"
type B = IsString<number>;   // Type B is "No"
type C = IsString<any>;      // Type C is "Yes" | "No" (distributive conditional type)
type D = IsString<string | number>; // Type D is "Yes" | "No" (distributive conditional type)

نکته مهم در مورد Conditional Types، رفتار توزیعی (Distributive) آن‌هاست. وقتی یک Conditional Type بر روی یک Union Type اعمال می‌شود، Conditional Type به صورت جداگانه بر روی هر عضو Union اعمال می‌شود و سپس نتایج به یک Union Type جدید تبدیل می‌شوند. این رفتار اغلب برای ساخت Utility Typeهایی مانند Exclude و Extract استفاده می‌شود.

type Exclude<T, U> = T extends U ? never : T;
// If T is assignable to U, it's excluded (becomes never), otherwise it's kept.

type NonNullableStrings = Exclude<"a" | "b" | undefined | null, undefined | null>;
// Equivalent to:
// ("a" extends (undefined | null) ? never : "a") |
// ("b" extends (undefined | null) ? never : "b") |
// (undefined extends (undefined | null) ? never : undefined) |
// (null extends (undefined | null) ? never : null)
// Result: "a" | "b" | never | never => "a" | "b"

کلمه کلیدی infer و مثال‌های پیچیده آن

کلمه کلیدی infer در Conditional Types برای استخراج (Infer) یک Type از یک Type دیگر استفاده می‌شود و آن را در یک Type Variable جدید “catch” می‌کند. این قابلیت به طرز باورنکردنی قدرتمند است و به شما امکان می‌دهد Typeهای بسیار پیچیده را تجزیه و تحلیل کنید.

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

function greet(name: string): string {
  return `Hello, ${name}`;
}

type GreetReturnType = ReturnType<typeof greet>; // GreetReturnType is string

در این مثال، infer R به تایپ‌اسکریپت می‌گوید: “اگر T یک تابع است که هر تعداد آرگومان را می‌گیرد و چیزی را برمی‌گرداند، آن چیزی که برمی‌گرداند را به عنوان R استخراج کن.” اگر شرط مطابقت داشته باشد، R Type بازگشتی تابع خواهد بود؛ در غیر این صورت، any خواهد بود.

مثال‌های پیشرفته‌تر با infer:

  • استخراج نوع عناصر آرایه:
  • type ElementType<T> = T extends (infer U)[] ? U : T;
    
      type ArrayElement = ElementType<number[]>; // ArrayElement is number
      type NonArrayType = ElementType<string>;   // NonArrayType is string
  • استخراج نوع مقدار Promise:
  • type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
    
      type PromiseValue = UnpackPromise<Promise<number>>; // PromiseValue is number
      type NonPromiseValue = UnpackPromise<string>;      // NonPromiseValue is string
  • استخراج نوع پارامترهای تابع:
  • type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
    
      type MyFunctionParams = Parameters<(a: number, b: string) => void>; // MyFunctionParams is [number, string]

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

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

نقشه‌برداری از Typeها: Mapped Types برای تحولات ساختاری

Mapped Types (Typeهای نقشه‌برداری شده) در تایپ‌اسکریپت ابزاری قدرتمند برای تبدیل Typeهای شیء موجود به Typeهای شیء جدید بر اساس یک الگوی تکراری هستند. آن‌ها به شما اجازه می‌دهند تا Propertyهای یک Type را پیمایش کرده و Propertyهای متناظر در یک Type جدید را تغییر دهید. این قابلیت به ویژه برای ایجاد Utility Typeهایی که Propertyهای یک شیء را Optional، Readonly یا تغییر Type می‌دهند، بسیار مفید است.

سینتکس [P in K]

سینتکس اصلی Mapped Type به شکل [P in K] است، که در آن K یک Union Type از کلیدها (Property Nameها) و P یک Type Variable است که هر کلید را در Union Type K پیمایش می‌کند. معمولاً K از keyof SomeType به دست می‌آید.

type MyMappedType<T> = {
  [P in keyof T]: T[P]; // This just recreates T, for demonstration
};

interface User {
  id: number;
  name: string;
  email?: string;
}

type CopiedUser = MyMappedType<User>;
// CopiedUser is { id: number; name: string; email?: string; }

Modifiers: +?, -?, +readonly, -readonly

Mapped Types همچنین امکان اضافه کردن یا حذف کردن Modifierها را از Propertyها فراهم می‌کنند. این Modifierها شامل ? (Optionality) و readonly هستند. شما می‌توانید با استفاده از + برای اضافه کردن و - برای حذف کردن آن‌ها را کنترل کنید. اگر هیچ + یا - استفاده نشود، Modifierها از Type اصلی به ارث می‌رسند.

  • +? (اضافه کردن Optionality): همه Propertyها را Optional می‌کند.
  • -? (حذف Optionality): همه Propertyها را Required می‌کند.
  • +readonly (اضافه کردن Readonly): همه Propertyها را Readonly می‌کند.
  • -readonly (حذف Readonly): Readonly بودن Propertyها را حذف می‌کند (آن‌ها را Mutable می‌کند).
interface Person {
  readonly name: string;
  age: number;
  address?: string;
}

// Make all properties optional:
type Partial<T> = {
  [P in keyof T]?: T[P]; // Shorthand for [P in keyof T]: T[P] | undefined;
};
type MyPartialPerson = Partial<Person>;
// MyPartialPerson is { name?: string; age?: number; address?: string; }

// Make all properties required:
type Required<T> = {
  [P in keyof T]-?: T[P];
};
type MyRequiredPerson = Required<Person>;
// MyRequiredPerson is { name: string; age: number; address: string; }

// Make all properties readonly:
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
type MyReadonlyPerson = Readonly<Person>;
// MyReadonlyPerson is { readonly name: string; readonly age: number; readonly address?: string; }

// Make all properties mutable (remove readonly):
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};
type MyMutablePerson = Mutable<Person>;
// MyMutablePerson is { name: string; age: number; address?: string; }

as remapping key names

در تایپ‌اسکریپت 4.1 به بعد، قابلیت as به Mapped Types اضافه شد که به شما اجازه می‌دهد نام کلیدهای جدید را تغییر نام دهید یا آن‌ها را فیلتر کنید. این قابلیت بسیار قدرتمند است و امکان تبدیل‌های پیچیده‌تر Type را فراهم می‌کند.

type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

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

type UserGetters = Getters<UserData>;
/*
UserGetters is:
{
  getName: () => string;
  getAge: () => number;
}
*/

در این مثال، `get${Capitalize<string & P>}` یک Template Literal Type است که هر کلید را به یک نام جدید تبدیل می‌کند (مثلاً name به getName). Capitalize<string & P> یک Utility Type داخلی است که حرف اول یک رشته را بزرگ می‌کند.

همچنین می‌توانید از as برای فیلتر کردن Propertyها استفاده کنید:

type OnlyStrings<T> = {
  [P in keyof T as T[P] extends string ? P : never]: T[P];
};

interface MixedType {
  name: string;
  age: number;
  email: string;
  isAdmin: boolean;
}

type StringProperties = OnlyStrings<MixedType>;
/*
StringProperties is:
{
  name: string;
  email: string;
}
*/

در اینجا، T[P] extends string ? P : never یک Conditional Type است که اگر Type یک Property رشته باشد، کلید آن را حفظ می‌کند و در غیر این صورت آن را به never تبدیل می‌کند. Propertyهایی که به never نگاشت می‌شوند، به طور موثر از Type نتیجه حذف می‌شوند.

کاربردها در تبدیل Property ها (مثل Pick, Omit)

بسیاری از Utility Typeهای داخلی تایپ‌اسکریپت مانند Pick و Omit، خود از Mapped Types و Conditional Types استفاده می‌کنند.

  • Pick<T, K>: یک Type جدید را با انتخاب زیرمجموعه‌ای از Propertyهای K از Type T می‌سازد.
  • type Pick<T, K extends keyof T> = {
        [P in K]: T[P];
    };
    
    interface Product {
      id: string;
      name: string;
      price: number;
      description: string;
    }
    
    type ProductSummary = Pick<Product, "id" | "name">;
    // ProductSummary is { id: string; name: string; }
  • Omit<T, K>: یک Type جدید را با حذف زیرمجموعه‌ای از Propertyهای K از Type T می‌سازد. (این با Exclude و Mapped Types پیاده‌سازی می‌شود)
  • type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
    
      type ProductDetails = Omit<Product, "id" | "description">;
      // ProductDetails is { name: string; price: number; }

Mapped Types ابزاری فوق‌العاده قدرتمند برای دستکاری Typeها و ایجاد Typeهای پویا بر اساس Typeهای موجود هستند. آن‌ها به شما اجازه می‌دهند تا Typeهای خود را Modular کنید و از تکرار کد جلوگیری نمایید، در حالی که Type Safety را در سراسر برنامه شما تضمین می‌کنند.

پیوستن به هم: Union و Intersection Types و مدیریت آن‌ها

Union Types (Typeهای اجتماع) و Intersection Types (Typeهای اشتراک) دو مفهوم بنیادین در تایپ‌اسکریپت هستند که به شما امکان می‌دهند تا Typeها را ترکیب کنید تا Typeهای جدید و پیچیده‌تر ایجاد کنید. این قابلیت‌ها برای مدل‌سازی سناریوهای داده‌ای که شامل چندین شکل احتمالی یا ترکیبی از چندین رفتار هستند، ضروری‌اند.

Union Types (`A | B`): کار با Typeهای مختلف، Discriminated Unions

یک Union Type، با استفاده از علامت |، نشان می‌دهد که یک متغیر می‌تواند یکی از چندین Type ممکن باشد. این برای سناریوهایی که یک مقدار می‌تواند از چندین Type مجاز باشد، بسیار مفید است.

type StringOrNumber = string | number;

let value: StringOrNumber;
value = "hello"; // OK
value = 123;     // OK
// value = true;    // Error: Type 'boolean' is not assignable to type 'StringOrNumber'.

وقتی با یک Union Type کار می‌کنید، تایپ‌اسکریپت تنها به Propertyها یا متدهایی اجازه دسترسی می‌دهد که در تمام اعضای Union مشترک باشند. برای دسترسی به اعضای غیر مشترک، نیاز به Narrowing (محدود کردن) Type دارید.

function printId(id: string | number) {
  console.log(id.toString()); // OK, toString() exists on both string and number
  // console.log(id.toUpperCase()); // Error: Property 'toUpperCase' does not exist on type 'string | number'.
                                 // Property 'toUpperCase' does not exist on type 'number'.
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // OK, now 'id' is narrowed to 'string'
  }
}
Discriminated Unions

یکی از قوی‌ترین کاربردهای Union Types، Discriminated Unions است. این الگو شامل Union Typeهایی از اشیاء است که هر کدام دارای یک Property مشترک (discriminant property) با یک Literal Type منحصر به فرد هستند. این Property مشترک به تایپ‌اسکریپت اجازه می‌دهد تا به طور هوشمندانه Type را Narrowing کند.

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;

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:
      // Exhaustive checking for switch statement (explained below)
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

const myCircle: Circle = { kind: "circle", radius: 10 };
console.log(getArea(myCircle)); // 314.159...

در مثال بالا، kind یک Discriminant Property است. تایپ‌اسکریپت از آن برای فهمیدن اینکه در هر case از switch، shape دقیقاً کدام Type از Union است، استفاده می‌کند.

Intersection Types (`A & B`): ترکیب Typeها، ایجاد Typeهای جدید

یک Intersection Type، با استفاده از علامت &، دو یا چند Type را ترکیب می‌کند تا یک Type جدید ایجاد کند که تمام Propertyهای Typeهای اصلی را دارد. این به معنای “هم A و هم B” است.

interface HasID {
  id: number;
}

interface HasName {
  name: string;
}

type PersonWithIDAndName = HasID & HasName;

const person: PersonWithIDAndName = {
  id: 1,
  name: "Alice"
};

// type PersonWithIDAndName is { id: number; name: string; }

اگر Typeهای ترکیب شده Propertyهای مشترکی با Typeهای ناسازگار داشته باشند، Property مشترک به never تبدیل می‌شود.

interface ConflictingTypeA {
  value: string;
}

interface ConflictingTypeB {
  value: number;
}

type Conflict = ConflictingTypeA & ConflictingTypeB; // Conflict is { value: never; }

// const conflict: Conflict = { value: "hello" }; // Error: Type 'string' is not assignable to type 'never'.
// const conflict2: Conflict = { value: 123 }; // Error: Type 'number' is not assignable to type 'never'.

Intersection Types اغلب برای ترکیب Mixins یا برای افزودن قابلیت‌های خاص به یک Type موجود بدون نیاز به ارث‌بری کلاس استفاده می‌شوند.

interface Loggable {
  log(message: string): void;
}

class UserProfile {
  constructor(public username: string, public email: string) {}
}

type EnhancedUser = UserProfile & Loggable;

function createEnhancedUser(username: string, email: string): EnhancedUser {
  return Object.assign(new UserProfile(username, email), {
    log: (message: string) => console.log(`[${username}]: ${message}`)
  });
}

const admin = createEnhancedUser("admin", "admin@example.com");
admin.log("User logged in.");
console.log(admin.username);

Exhaustive Checking با never

never Type در تایپ‌اسکریپت نشان‌دهنده Typeی است که هرگز نباید رخ دهد. این Type معمولاً برای نشان دادن توابعی استفاده می‌شود که هرگز باز نمی‌گردند (مثلاً توابعی که یک خطا را پرتاب می‌کنند یا یک حلقه بی‌نهایت اجرا می‌کنند). در Conditional Types، never برای حذف Propertyها استفاده می‌شود.

یکی از کاربردهای مهم never، در ترکیب با Discriminated Unions، برای اطمینان از پوشش کامل تمام حالات ممکن در یک switch یا if/else if است. این الگو به “Exhaustive Checking” معروف است.

function getAreaSafe(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    // case "triangle": // If we comment this out...
    //   return 0.5 * shape.base * shape.height;
    default:
      const _exhaustiveCheck: never = shape; // If a new Shape type is added, 'shape' will not be 'never' here
      return _exhaustiveCheck; // TypeScript will show an error here
  }
}

اگر یک case جدید به Union Type Shape اضافه کنید (مثلاً interface Pentagon { kind: "pentagon"; ... }) اما فراموش کنید که آن را در تابع getAreaSafe مدیریت کنید، تایپ‌اسکریپت در خط const _exhaustiveCheck: never = shape; خطا می‌دهد. این به شما اطمینان می‌دهد که تمام حالات ممکن Type را مدیریت کرده‌اید و از خطاهای زمان اجرا جلوگیری می‌کند.

Type Guards و Type Predicates برایNarrowing Type

Type Guards توابع یا عباراتی هستند که تایپ‌اسکریپت از آن‌ها برای Narrowing Type یک متغیر در یک Scope خاص استفاده می‌کند. رایج‌ترین Type Guards شامل typeof و instanceof هستند.

function logValue(x: string | Date) {
  if (typeof x === "string") {
    console.log(x.toUpperCase()); // x is string
  } else if (x instanceof Date) {
    console.log(x.toDateString()); // x is Date
  }
}

User-Defined Type Guards (Type Predicates):
شما می‌توانید Type Guardهای خود را با استفاده از Type Predicateها تعریف کنید. یک Type Predicate به شکل parameterName is Type نوشته می‌شود و تایپ‌اسکریپت را متقاعد می‌کند که اگر تابع true برگرداند، پارامتر مورد نظر از Type مشخص شده است.

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

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

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

function getPetMoves(pet: Bird | Fish) {
  if (isFish(pet)) {
    pet.swim(); // pet is narrowed to Fish
  } else {
    pet.fly(); // pet is narrowed to Bird
  }
}

in Operator نیز می‌تواند به عنوان یک Type Guard عمل کند:

function checkAnimal(animal: { swim?: () => void; fly?: () => void }) {
  if ("swim" in animal) {
    animal.swim(); // animal is inferred to have swim()
  } else if ("fly" in animal) {
    animal.fly(); // animal is inferred to have fly()
  }
}

Union و Intersection Types، به همراه Type Guardها و Exhaustive Checking، ابزارهایی قدرتمند برای مدل‌سازی داده‌های پیچیده و اطمینان از صحت منطق برنامه شما در تایپ‌اسکریپت هستند.

فراتر از نوع‌بندی: Template Literal Types و Const Assertions

تایپ‌اسکریپت به طور مداوم با ویژگی‌های جدیدی تکامل می‌یابد که Type System آن را حتی قدرتمندتر و Expressiveتر می‌کنند. دو مورد از این ویژگی‌های نسبتاً جدیدتر، Template Literal Types و Const Assertions هستند که امکان Type Checking دقیق‌تر و ایجاد Typeهای پویا را فراهم می‌کنند.

Template Literal Types: ترکیب رشته‌ها با Typeها

Template Literal Types، که در تایپ‌اسکریپت 4.1 معرفی شدند، به شما امکان می‌دهند تا Typeهای رشته‌ای جدیدی را با استفاده از ترکیب Literal String Typeها با Typeهای دیگر در یک قالب (Template) ایجاد کنید، درست مانند Template Literals در جاوااسکریپت. این ویژگی به ویژه برای کار با String-based APIs، تولید نام رویدادها، یا ساخت URLها مفید است.

type EventName<T extends string> = `${T}Changed` | `${T}Created` | `${T}Deleted`;

type UserEvents = EventName<"user">; // "userChanged" | "userCreated" | "userDeleted"
type ProductEvents = EventName<"product">; // "productChanged" | "productCreated" | "productDeleted"

function emitEvent(eventName: UserEvents) {
  // ...
}

emitEvent("userCreated"); // OK
// emitEvent("userUpdated"); // Error: Type '"userUpdated"' is not assignable to type '"userChanged" | "userCreated" | "userDeleted"'.

همچنین می‌توانید از Union Types در داخل Template Literal Types استفاده کنید تا تمام ترکیب‌های ممکن را ایجاد کنید:

type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "users" | "products";

type APIPath = `/${Endpoint}/${number | "all"}` | `/status`;

type RequestPath = `${HTTPMethod} ${APIPath}`;

type UserRequests = RequestPath;
/*
UserRequests is:
"GET /users/1" | "GET /users/all" | "GET /products/1" | "GET /products/all" | "GET /status" |
"POST /users/1" | "POST /users/all" | "POST /products/1" | "POST /products/all" | "POST /status" |
// ... and so on for PUT and DELETE
*/

function handleRequest(path: RequestPath) {
  // ...
}

handleRequest("GET /users/all"); // OK
handleRequest("POST /products/123"); // OK
// handleRequest("PATCH /users/all"); // Error: Type '"PATCH /users/all"' is not assignable to type 'RequestPath'.

Template Literal Types با Utility Typeهای داخلی مانند Uppercase<StringType>، Lowercase<StringType>، Capitalize<StringType> و Uncapitalize<StringType> نیز ترکیب می‌شوند تا قابلیت‌های تغییر رشته را ارائه دهند:

type Key = "name" | "age";
type SetterMethodNames = `set${Capitalize<Key>}`;
// SetterMethodNames is "setName" | "setAge"

این ویژگی‌ها به شما امکان می‌دهند تا Typeهای بسیار دقیق و خودکار ایجاد کنید که به طور خودکار به تغییرات در Literal Stringها واکنش نشان می‌دهند، و خطاهای زمان اجرا مربوط به نام‌های اشتباه را کاهش می‌دهند.

Const Assertions (`as const`): تبدیل متغیرها به Literal Typeها

as const یک Type Assertion است که در تایپ‌اسکریپت 3.4 معرفی شد. وقتی as const را به یک Literal (یا یک آرایه یا شیء حاوی Literalها) اعمال می‌کنید، تایپ‌اسکریپت Type آن را به دقیق‌ترین و محدودترین شکل ممکن استنتاج می‌کند. این بدان معناست که:

  • Literal Primitiveها (مانند رشته‌ها، اعداد، بولین‌ها) به Literal Typeهای خودشان تبدیل می‌شوند، نه به Typeهای گسترده‌تر (string، number، boolean).
  • Propertyهای شیء به readonly تبدیل می‌شوند.
  • عناصر آرایه به readonly tuple تبدیل می‌شوند.

بدون as const:

const COLORS_NORMAL = {
  red: "#FF0000",
  green: "#00FF00",
};

type ColorsNormalType = typeof COLORS_NORMAL;
/*
ColorsNormalType is:
{
  red: string;
  green: string;
}
*/
// COLORS_NORMAL.red = "#FF0000"; // OK, property is mutable
// COLORS_NORMAL.red = "any other string"; // Also OK

با as const:

const COLORS_CONST = {
  red: "#FF0000",
  green: "#00FF00",
} as const;

type ColorsConstType = typeof COLORS_CONST;
/*
ColorsConstType is:
{
  readonly red: "#FF0000";
  readonly green: "#00FF00";
}
*/
// COLORS_CONST.red = "#FF0000"; // Error: Cannot assign to 'red' because it is a read-only property.
// COLORS_CONST.red = "any other string"; // Error: Type '"any other string"' is not assignable to type '"#FF0000"'.

as const به ویژه برای تعریف مجموعه‌ای از ثابت‌ها یا تنظیمات که Typeهای آن‌ها باید به صورت Literal و غیرقابل تغییر باشند، بسیار مفید است. این کار امکان Type Checking دقیق‌تر را فراهم می‌کند و از خطاهای ناشی از تغییر تصادفی مقادیر جلوگیری می‌کند.

پیامدهای as const بر Type Inference

as const به طور چشمگیری بر Type Inference تأثیر می‌گذارد. به جای استنتاج Typeهای گسترده (widening)، تایپ‌اسکریپت Typeهای دقیق (narrowing) را استنتاج می‌کند. این به شما اجازه می‌دهد تا Typeهای بسیار دقیق و محدودتری را بر اساس داده‌های زمان کامپایل ایجاد کنید.

const roles = ["admin", "editor", "viewer"]; // Type is string[]
// roles.push("guest"); // OK

const immutableRoles = ["admin", "editor", "viewer"] as const; // Type is readonly ["admin", "editor", "viewer"]
// immutableRoles.push("guest"); // Error: Property 'push' does not exist on type 'readonly ["admin", "editor", "viewer"]'.

type Role = typeof immutableRoles[number]; // "admin" | "editor" | "viewer"

function assignRole(user: string, role: Role) {
  console.log(`Assigning ${role} to ${user}`);
}

assignRole("John", "admin"); // OK
// assignRole("Jane", "guest"); // Error: Argument of type '"guest"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.

در مثال بالا، بدون as const، roles Type string[] را می‌گرفت که اجازه افزودن هر رشته‌ای را می‌داد. اما با as const، immutableRoles به یک Tuple از Literal Typeها تبدیل می‌شود که دقیقاً مقادیر موجود را نشان می‌دهد و اجازه تغییر یا افزودن عناصر غیرمجاز را نمی‌دهد. سپس می‌توانیم یک Union Type از این Literalها را برای استفاده در Type Checking دیگر استخراج کنیم.

Template Literal Types و Const Assertions ابزارهایی پیشرفته هستند که قابلیت‌های Type System تایپ‌اسکریپت را به طور قابل توجهی گسترش می‌دهند و به توسعه‌دهندگان اجازه می‌دهند تا Typeهای دقیق‌تر و ایمن‌تری را در سناریوهای پیچیده‌تر و پویا تعریف کنند.

سایر ابزارهای پیشرفته و نکات مهم

سیستم نوع‌بندی تایپ‌اسکریپت مجموعه‌ای غنی از Utility Typeهای داخلی و مفاهیم پیشرفته دیگر را ارائه می‌دهد که برای سناریوهای خاص توسعه بسیار مفید هستند.

Utility Types داخلی تایپ‌اسکریپت

تایپ‌اسکریپت مجموعه‌ای از Utility Typeهای از پیش تعریف شده را برای انجام عملیات‌های رایج بر روی Typeها فراهم می‌کند. قبلاً به برخی از آن‌ها مانند Partial، Required، Readonly، Pick و Omit اشاره شد. در ادامه به معرفی و توضیح برخی دیگر می‌پردازیم:

  • Exclude<T, U>: Typeهایی را از T حذف می‌کند که قابل انتساب به U هستند.

    type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
    type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
    type T2 = Exclude<string | number | (() => void), Function>; // string | number
  • Extract<T, U>: Typeهایی را از T استخراج می‌کند که قابل انتساب به U هستند.

    type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
    type T1 = Extract<string | number | (() => void), Function>; // () => void
  • NonNullable<T>: null و undefined را از T حذف می‌کند.

    type T0 = NonNullable<string | number | undefined>; // string | number
    type T1 = NonNullable<string[] | null | undefined>; // string[]
  • Parameters<T>: Typeهای پارامترهای یک تابع Type T را به عنوان یک Tuple Type برمی‌گرداند.

    declare function f1(arg: { a: number; b: string }): void;
    type T0 = Parameters<() => string>; // []
    type T1 = Parameters<(s: string, n: number) => void>; // [string, number]
    type T2 = Parameters<typeof f1>; // [{ a: number, b: string }]
  • ReturnType<T>: Type بازگشتی یک تابع Type T را برمی‌گرداند.

    declare function f1(): { a: number; b: string };
    type T0 = ReturnType<() => string>; // string
    type T1 = ReturnType<(s: string) => void>; // void
    type T2 = ReturnType<typeof f1>; // { a: number, b: string }
  • Awaited<T>: Type را از Promiseها باز می‌کند.

    type P1 = Promise<string>;
    type R1 = Awaited<P1>; // string
    
    type P2 = Promise<Promise<number>>;
    type R2 = Awaited<P2>; // number (unwraps recursively)

این Utility Typeها کارهای تکراری را خودکار می‌کنند و به توسعه‌دهندگان اجازه می‌دهند تا بر روی منطق کسب و کار تمرکز کنند تا بر روی جزئیات Type Mapping.

unknown vs any و موارد استفاده از unknown

any و unknown هر دو می‌توانند هر مقداری را در خود جای دهند، اما تفاوت حیاتی در ایمنی Type آن‌هاست. any در واقع Type Checking را برای متغیر مربوطه غیرفعال می‌کند و هر عملیاتی روی آن مجاز است، حتی اگر Typeهای واقعی در زمان اجرا متفاوت باشند. این می‌تواند منجر به خطاهای زمان اجرا شود.

let valueAny: any = "hello";
valueAny.foo(); // No error, but will throw at runtime if foo() doesn't exist

در مقابل، unknown ایمن‌تر است. قبل از انجام هر عملیاتی بر روی یک متغیر از نوع unknown، باید Type آن را Narrowing کنید (با استفاده از Type Guardها). این تضمین می‌کند که کد شما Type Safe باقی می‌ماند.

let valueUnknown: unknown = "hello";
// valueUnknown.foo(); // Error: Object is of type 'unknown'.

if (typeof valueUnknown === "string") {
  console.log(valueUnknown.toUpperCase()); // OK, valueUnknown is now narrowed to string
}

unknown بهترین انتخاب برای مقادیر ناشناخته‌ای است که از خارج از برنامه شما می‌آیند (مانند ورودی API یا خواندن فایل)، زیرا شما را مجبور می‌کند که Type آن را بررسی کنید و به طور ایمن با آن رفتار کنید. any باید فقط در موارد بسیار نادر و زمانی که هیچ راه دیگری برای Type کردن کد وجود ندارد، استفاده شود.

never Type و کاربردهای آن

همانطور که قبلاً اشاره شد، never Type نشان دهنده Typeی است که هرگز نباید رخ دهد. کاربردهای اصلی آن عبارتند از:

  • توابعی که هرگز باز نمی‌گردند: توابعی که همیشه یک خطا پرتاب می‌کنند یا وارد یک حلقه بی‌نهایت می‌شوند.
  • function error(message: string): never {
      throw new Error(message);
    }
    
    function infiniteLoop(): never {
      while (true) {}
    }
  • حذف Typeها در Conditional Types: همانطور که در Exclude دیدیم، never برای حذف اعضا از Union Typeها استفاده می‌شود.
  • Exhaustive Checking: برای اطمینان از پوشش تمام حالات در switch یا if/else.

Non-null Assertion Operator (`!`) و Type Assertion (`as` keyword)

  • Non-null Assertion Operator (`!`): این اپراتور به تایپ‌اسکریپت می‌گوید که “من می‌دانم این مقدار null یا undefined نیست، حتی اگر تایپ‌اسکریپت فکر کند ممکن است باشد.” از آن فقط زمانی استفاده کنید که 100% مطمئن هستید، زیرا اگر اشتباه کنید، یک خطای زمان اجرا رخ خواهد داد.

    function greetUser(name: string | null) {
      const userName: string = name!; // Assert that name is not null
      console.log(`Hello, ${userName.toUpperCase()}`);
    }
    
    // const element = document.getElementById("my-element");
    // const text = element!.textContent; // Use with caution! What if element is null?
  • Type Assertion (`as` keyword): این به کامپایلر تایپ‌اسکریپت می‌گوید که “من می‌دانم Type این مقدار چیست، پس به من اعتماد کن.” این یک عملیات زمان اجرا نیست و فقط برای Type Checking در زمان کامپایل استفاده می‌شود.

    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length; // Assert as string
    
    interface Foo {
      bar: number;
      baz: string;
    }
    
    const obj = { bar: 123, baz: "hello", extra: true };
    const foo: Foo = obj as Foo; // Assert that obj conforms to Foo
    // Note: This won't check for missing properties, only extra.
    // If you remove 'bar' or 'baz' from 'obj', this assertion would still pass at compile-time but fail at runtime.

    از Type Assertion با احتیاط استفاده کنید. اغلب بهتر است از Type Guardها یا Type Inference برای اجازه دادن به تایپ‌اسکریپت برای فهمیدن Type استفاده کنید.

Tuple Types در عمق

Tuple Types آرایه‌هایی با تعداد مشخصی از عناصر با Typeهای مشخص در هر موقعیت هستند. آن‌ها برای مدل‌سازی ساختارهای داده‌ای که شبیه آرایه هستند اما دارای معنای خاصی برای هر عنصر هستند، مفیدند.

type Rgb = [number, number, number];
const red: Rgb = [255, 0, 0];
// const invalidColor: Rgb = [255, 0, 0, 1]; // Error: Type 'number' is not assignable to type 'undefined'.

Tupleها می‌توانند شامل عناصر Optional (با ?) و Rest Elements (با ...) باشند:

type OptionalTuple = [number, string?];
const t1: OptionalTuple = [10];        // OK
const t2: OptionalTuple = [10, "hello"]; // OK

type RestTuple = [number, ...string[]];
const r1: RestTuple = [1, "a", "b", "c"]; // OK
const r2: RestTuple = [1];               // OK

Enums (نکات پیشرفته)

Enums (Enumerations) به شما امکان می‌دهند تا مجموعه‌ای از ثابت‌های نام‌گذاری شده را تعریف کنید. آن‌ها می‌توانند عددی (Numeric) یا رشته‌ای (String) باشند. اگرچه در تایپ‌اسکریپت رایج هستند، استفاده از Literal Union Types اغلب توصیه می‌شود، زیرا Type Safeتر هستند و نیازی به کد جاوااسکریپت زمان اجرا ندارند.

// Numeric Enum (default)
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right  // 3
}

// String Enum
enum HttpMethod {
  GET = "GET",
  POST = "POST",
  PUT = "PUT_METHOD",
}

// Literal Union Type (often preferred for better Type Safety and smaller bundle size)
type UserRole = "admin" | "editor" | "viewer";

Enums به خصوص در زمانی که نیاز به Mapping بین مقادیر عددی و نام‌ها دارید (مثلاً با مقادیر APIهای قدیمی) مفیدند، اما برای استفاده‌های جدید، Literal Union Types معمولاً ارجحیت دارند.

نتیجه‌گیری

سیستم نوع‌بندی پیشرفته تایپ‌اسکریپت فراتر از Type Checking ساده است؛ این یک ابزار قدرتمند برای طراحی نرم‌افزار، مدل‌سازی داده‌ها و بهبود کیفیت کد در طول چرخه توسعه است. با درک و به‌کارگیری مفاهیمی مانند Generic ها، Conditional Types، Mapped Types، Template Literal Types و Const Assertions، توسعه‌دهندگان می‌توانند کدهای JavaScript را با سطحی از دقت و امنیت بنویسند که قبلاً غیرقابل تصور بود.

قابلیت‌هایی مانند Discriminated Unions و Exhaustive Checking به شما اطمینان می‌دهند که تمام حالات ممکن داده‌ها را مدیریت کرده‌اید، در حالی که Utility Types داخلی و Type Guardهای سفارشی‌سازی شده فرآیند Narrowing Type و دستکاری Type را ساده می‌کنند. هدف نهایی، نوشتن کدی است که نه تنها کار می‌کند، بلکه قابل نگهداری، مقیاس‌پذیر و در برابر خطاها مقاوم باشد. تسلط بر این ویژگی‌های پیشرفته تایپ‌اسکریپت، شما را به یک توسعه‌دهنده کارآمدتر و مؤثرتر تبدیل خواهد کرد که قادر به ساخت برنامه‌های کاربردی پیچیده و پایدار است.

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

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

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

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

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

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

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

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

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