TypeScript: الأنواع العامة المتقدمة في TypeScript

آخر تحديث: 2026-08-26

الآن بعد أن أتقنت أساسيات الأنواع العامة، ينتقل هذا الدرس إلى موضوعات أكثر تقدماً — الأنواع الشرطية، infer، والأنواع المُخططة. وتُعد هذه أدوات أساسية للبرمجة القائمة على الأنواع في TypeScript، وتشكل الأساس لفهم شفرة المصدر لأنواع الأدوات المدمجة.

1. الأنواع الشرطية

تختار الأنواع الشرطية نتائج مختلفة بناءً على شروط النوع — على غرار العامل الثلاثي على مستوى النوع:

(1) قواعد النحو الأساسية

TYPESCRIPT
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;   // "yes"
type B = IsString<number>;   // "no"
type C = IsString<"hello">;  // "yes" ("hello" is a string subtype of)

(2) الأنواع الشرطية وأنواع الاتحاد (موزعة)

عندما يكون T نوعًا تجميعيًا، يقوم النوع الشرطي بتقييم كل عنصر على حدة بطريقة «موزعة»:

TYPESCRIPT
type ToString<T> = T extends string ? "string" : "other";

// T = string | number → Evaluate each one separately → "string" | "other"
type Result = ToString<string | number>;  // "string" | "other"

(3) تعطيل السلوك الموزع

ضع الكود بين علامتي [T] لتعطيل التقييم الموزع:

TYPESCRIPT
type IsNever<T> = [T] extends [never] ? "yes" : "no";

type A = IsNever<never>;           // "yes"
type B = IsNever<string | never>;  // "no"(No longer available)

▶ مثال: تسوية المصفوفات مع ضمان سلامة الأنواع

TYPESCRIPT
type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>;        // string
type B = Flatten<number>;          // number
type C = Flatten<boolean[]>;       // boolean
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
// Executed successfully


2. الكلمة الرئيسية infer

infer «استنتاج» متغير النوع في شرط النوع — وهو أحد أقوى الأدوات في البرمجة النمطية بلغة TypeScript:

(1) استنتاج نوع عناصر المصفوفة

TYPESCRIPT
type ArrayElement<T> = T extends (infer E)[] ? E : never;

type A = ArrayElement<string[]>;   // string
type B = ArrayElement<number[]>;   // number
type C = ArrayElement<string>;     // never(string Not an array)

(2) استنتاج نوع قيمة الإرجاع للدالة

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

type A = GetReturnType<() => string>;          // string
type B = GetReturnType<(x: number) => boolean>; // boolean
type C = GetReturnType<(x: string) => void>;    // void
type D = GetReturnType<string>;                  // never(string Not a function)

(3) استنتاج أنواع معلمات الدالة

TYPESCRIPT
type GetParameters<T> = T extends (...args: infer P) => any ? P : never;

type A = GetParameters<(a: string, b: number) => void>;  // [string, number]
type B = GetParameters<() => void>;                        // []
type C = GetParameters<(x: boolean) => string>;            // [boolean]

(4) استنتاج نوع القيمة التي تم تحديدها للوعد

TYPESCRIPT
type Awaited<T> = T extends Promise<infer U> ? U : T;

type A = Awaited<Promise<string>>;     // string
type B = Awaited<Promise<number[]>>;   // number[]
type C = Awaited<string>;              // string(No Promise,Return directly)

// nested Promise——Recursive Unpacking
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;

type D = DeepAwaited<Promise<Promise<string>>>;  // string


3. الأنواع المُعينة

أنواع الخرائط: إنشاء نوع جديد استنادًا إلى نوع قديم — تطبيق التحويلات على كل خاصية:

(1) قواعد النحو الأساسية

TYPESCRIPT
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

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

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

type OptionalUser = Optional<User>;
// { name?: string; age?: number; email?: string }

(2) مُعدِّلات التعيين

استخدم +/- لإضافة أو إزالة المُعدِّلات:

TYPESCRIPT
// Remove readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// Remove (optional)(?)
type Required2<T> = {
  [K in keyof T]-?: T[K];
};

interface Config {
  readonly host: string;
  readonly port: number;
  debug?: boolean;
}

type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean }  —— readonly Removed

type RequiredConfig = Required2<Config>;
// { readonly host: string; readonly port: number; debug: boolean }  —— ? Removed

(3) إعادة تعيين المفاتيح (TypeScript 4.1+)

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

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

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

(4) خصائص المرشح

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

interface Mixed {
  name: string;
  age: number;
  email: string;
  active: boolean;
}

type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }  —— Keep only string Type Properties

▶ مثال: إنشاء عميل واجهة برمجة تطبيقات آمن من حيث الأنواع

TYPESCRIPT
// Definition API Routing Type
type ApiRoutes = {
  "/users": { response: { id: number; name: string }[] };
  "/users/:id": { response: { id: number; name: string }; params: { id: number } };
  "/posts": { response: { id: number; title: string }[] };
};

// Extract Response Type Based on Route
type ApiResponse<R extends keyof ApiRoutes> = ApiRoutes[R]["response"];

type UsersResponse = ApiResponse<"/users">;
// { id: number; name: string }[]

type UserResponse = ApiResponse<"/users/:id">;
// { id: number; name: string }

// Type-safe fetch Function
function fetchApi<R extends keyof ApiRoutes>(
  route: R,
  ...args: "params" extends keyof ApiRoutes[R]
    ? [params: ApiRoutes[R]["params"]]
    : []
): Promise<ApiResponse<R>> {
  // Implementation Omitted
  return {} as any;
}

// Automatically infer parameters and return types at runtime
// let users = fetchApi("/users");           // No parameters required
// let user = fetchApi("/users/:id", { id: 1 });  // Must be provided params
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
// Executed successfully


4. القيم الافتراضية للأنواع العامة

يمكن أن تحتوي معلمات الأنواع العامة على قيم افتراضية — حيث يُستخدم النوع الافتراضي عندما يتعذر استنتاج النوع أو لا تكون هناك حاجة إلى تحديده:

TYPESCRIPT
interface PaginatedResult<T, PageSize = 10> {
  data: T[];
  total: number;
  pageSize: PageSize;
}

// Use the default values
type UserResult = PaginatedResult<User>;
// data: User[]; total: number; pageSize: 10

// Override the default value
type CustomResult = PaginatedResult<User, 20>;
// data: User[]; total: number; pageSize: 20

(1) القيود المفروضة على القيم الافتراضية

يجب أن تأتي المعلمات التي تحتوي على قيم افتراضية بعد المعلمات التي لا تحتوي على قيم افتراضية:

TYPESCRIPT
// ✅ Correct
type A<T, U = string> = { first: T; second: U };

// ❌ Error——Those with default values U When there is no default value, T Previous
// type B<U = string, T> = { first: T; second: U };


5. التباين المشترك والثبات

هذا أحد أكثر المفاهيم المتقدمة إرباكًا في نظام أنواع TypeScript — عليك فهمه لتدرك سبب صحة بعض عمليات التعيين وعدم صحة أخرى.

(1) متغير مترافق

"إذا كان A نوعًا فرعيًا من B، فإن Container<A> يكون أيضًا نوعًا فرعيًا من Container<B>" — التغييرات في نفس الاتجاه:

TYPESCRIPT
// string is a subtype of string | number
// string[] Me too (string | number)[] subtypes of → Covariation
let strings: string[] = ["a", "b"];
let mixed: (string | number)[] = strings;  // ✅ Covariant Safety

(2) المرافق (المتباين)

أنواع معلمات الدوال تعمل بشكل معكوس — «إذا كان A نوعًا فرعيًا لـ B، فإن (B => void) يكون نوعًا فرعيًا لـ (A => void)» — حيث تسري العلاقة في الاتجاه المعاكس:

TYPESCRIPT
// string is a subtype of string | number
// (string | number => void) is a subtype of (string => void) → Inversion

type StringHandler = (arg: string) => void;
type MixedHandler = (arg: string | number) => void;

let mixedHandler: MixedHandler = (arg) => console.log(arg);
let stringHandler: StringHandler = mixedHandler;  // ✅ Inversion——Functions that handle broader types can be assigned to variables that handle narrower types.

(3) لماذا تم عكس ترتيب معلمات الدالة؟

TYPESCRIPT
// If the parameter is covariant——Unsafe
let dogHandler: (dog: Dog) => void = (dog) => dog.bark();
let animalHandler: (animal: Animal) => void = dogHandler;  // ❌ Danger!
// Call animalHandler(cat) → dog.bark() called on cat → Runtime Error

// Inverting is the safe option——Functions that handle broader types can safely handle narrower types.
💡 النقطة الأساسية: قيم إرجاع الدوال متغيرة بشكل متوافق (أمان أكثر تحديدًا لنوع القيمة المرجعة)، في حين أن معلمات الدوال متغيرة بشكل معاكس (أمان أكثر عمومية لنوع المعلمة). وتفرض TypeScript بصرامة عمليات التحقق من التغير المعاكس في وضع strictFunctionTypes.


▶ مثال: Building a Type-Safe API Client

الناتج:

TEXT 📖 للعرض فقط
42
TYPESCRIPT
// Definition API Routing Type
type ApiRoutes = {
  "/users": { response: { id: number; name: string }[] };
  "/users/:id": { response: { id: number; name: string }; params: { id: number } };
  "/posts": { response: { id: number; title: string }[] };
};

// Extract Response Type Based on Route
type ApiResponse<R extends keyof ApiRoutes> = ApiRoutes[R]["response"];

type UsersResponse = ApiResponse<"/users">;
// { id: number; name: string }[]

type UserResponse = ApiResponse<"/users/:id">;
// { id: number; name: string }

// Type-safe fetch Function
function fetchApi<R extends keyof ApiRoutes>(
  route: R,
  ...args: "params" extends keyof ApiRoutes[R]
    ? [params: ApiRoutes[R]["params"]]
    : []
): Promise<ApiResponse<R>> {
  // Implementation Omitted
  return {} as any;
}

// Automatically infer parameters and return types at runtime
  // let users = fetchApi("/users");           // No parameters required
  // let user = fetchApi("/users/:id", { id: 1 });  // Must be provided params

الناتج:

TEXT 📖 للعرض فقط
Type-safe API client — no runtime output (compile-time type checking only)

❓ أسئلة شائعة

س ما الفرق بين infer وT العام؟
ج T العام هو معلمة نوع يوفرها المستدعي، بينما infer هو متغير نوع يُستنتج تلقائيًا ضمن نوع شرطي. T تعني «أنت تخبرني»، وinfer تعني «أستنتجها بنفسي». ولا يمكن استخدام infer إلا في جملة extends من النوع الشرطي.
س ما هي العلاقة بين أنواع التعيين وأنواع الأدوات المساعدة (مثل Partial وRequired)؟
ج يتم تنفيذ أنواع الأدوات المساعدة باستخدام أنواع التعيين. تعد Partial وRequired وReadonly وPick وOmit وغيرها أنواع تعيين مدمجة في TypeScript. سيساعدك فهم كيفية عمل أنواع التعيين على فهم كيفية عمل أنواع الأدوات المساعدة هذه، كما ستتمكن أيضًا من إنشاء أنواع أدوات مساعدة مخصصة خاصة بك.
س هل التباين المشترك والتباين المعاكس مهمان في عملية التطوير اليومية؟
ج في معظم الأحيان، لا داعي للتفكير فيهما — فالتحقق من الأنواع في TypeScript يتعامل معهما تلقائيًا. ما عليك سوى فهمهما عند كتابة الدوال العامة، أو الدوال ذات الترتيب الأعلى، أو أدوات الأنواع. أما المبتدئون، فينبغي عليهم فقط التعرف على هذين المفهومين، والتعمق فيهما فقط عندما يواجهون حالات عدم تطابق الأنواع في تعيينات الدوال.
س متى يتم «توزيع» النوع الشرطي؟
ج عندما يكون الحرف T في النوع الشرطي معلمة نوع عارية (غير مغلفة في توبول أو كائن أو ما شابه) ويكون نوع اتحاد، يتم توزيع التقييم. أما تغليفه في [T] extends [U] فيؤدي إلى تعطيل التوزيع. يُعد مثال IsNever السيناريو الأكثر شيوعًا الذي يتطلب تعطيل التوزيع — حيث يتم تخطي never أثناء التوزيع عندما يكون عضوًا في الاتحاد.

📖 ملخص

📝 تمارين

  1. المشكلة الأساسية (الصعوبة ⭐): قم بتنفيذ IsArray<T> باستخدام الأنواع الشرطية — إذا كان T نوع مصفوفة، فقم بإرجاع true؛ وإلا، فقم بإرجاع false. اختبر IsArray<string[]> وIsArray<number>.
  2. مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ Stringify<T> باستخدام نوع التعيين — قم بتحويل أنواع جميع خصائص الكائن إلى string. على سبيل المثال، { age: number }{ age: string }.
  3. مشكلة التحدي (الصعوبة ⭐⭐⭐): استخدم infer والأنواع الشرطية لتنفيذ DeepPromise<T> — قم بفك تداخل الـ Promises المتداخلة بشكل متكرر حتى يتم الحصول على نوع غير Promise. يجب أن تُرجع حالة الاختبار DeepPromise<Promise<Promise<Promise<number>>>> عددًا.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%