TypeScript: الأنواع العامة المتقدمة في TypeScript
آخر تحديث: 2026-08-26
الآن بعد أن أتقنت أساسيات الأنواع العامة، ينتقل هذا الدرس إلى موضوعات أكثر تقدماً — الأنواع الشرطية، infer، والأنواع المُخططة. وتُعد هذه أدوات أساسية للبرمجة القائمة على الأنواع في TypeScript، وتشكل الأساس لفهم شفرة المصدر لأنواع الأدوات المدمجة.
1. الأنواع الشرطية
تختار الأنواع الشرطية نتائج مختلفة بناءً على شروط النوع — على غرار العامل الثلاثي على مستوى النوع:
(1) قواعد النحو الأساسية
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 نوعًا تجميعيًا، يقوم النوع الشرطي بتقييم كل عنصر على حدة بطريقة «موزعة»:
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] لتعطيل التقييم الموزع:
type IsNever<T> = [T] extends [never] ? "yes" : "no";
type A = IsNever<never>; // "yes"
type B = IsNever<string | never>; // "no"(No longer available)
▶ مثال: تسوية المصفوفات مع ضمان سلامة الأنواع
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
الناتج:
// Executed successfully
2. الكلمة الرئيسية infer
infer «استنتاج» متغير النوع في شرط النوع — وهو أحد أقوى الأدوات في البرمجة النمطية بلغة TypeScript:
(1) استنتاج نوع عناصر المصفوفة
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) استنتاج نوع قيمة الإرجاع للدالة
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) استنتاج أنواع معلمات الدالة
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) استنتاج نوع القيمة التي تم تحديدها للوعد
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) قواعد النحو الأساسية
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) مُعدِّلات التعيين
استخدم +/- لإضافة أو إزالة المُعدِّلات:
// 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+)
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) خصائص المرشح
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
▶ مثال: إنشاء عميل واجهة برمجة تطبيقات آمن من حيث الأنواع
// 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
الناتج:
// Executed successfully
4. القيم الافتراضية للأنواع العامة
يمكن أن تحتوي معلمات الأنواع العامة على قيم افتراضية — حيث يُستخدم النوع الافتراضي عندما يتعذر استنتاج النوع أو لا تكون هناك حاجة إلى تحديده:
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) القيود المفروضة على القيم الافتراضية
يجب أن تأتي المعلمات التي تحتوي على قيم افتراضية بعد المعلمات التي لا تحتوي على قيم افتراضية:
// ✅ 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>" — التغييرات في نفس الاتجاه:
// 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)» — حيث تسري العلاقة في الاتجاه المعاكس:
// 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) لماذا تم عكس ترتيب معلمات الدالة؟
// 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.
strictFunctionTypes.
▶ مثال: Building a Type-Safe API Client
الناتج:
42
// 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
الناتج:
Type-safe API client — no runtime output (compile-time type checking only)
❓ أسئلة شائعة
infer وT العام؟T العام هو معلمة نوع يوفرها المستدعي، بينما infer هو متغير نوع يُستنتج تلقائيًا ضمن نوع شرطي. T تعني «أنت تخبرني»، وinfer تعني «أستنتجها بنفسي». ولا يمكن استخدام infer إلا في جملة extends من النوع الشرطي.[T] extends [U] فيؤدي إلى تعطيل التوزيع. يُعد مثال IsNever السيناريو الأكثر شيوعًا الذي يتطلب تعطيل التوزيع — حيث يتم تخطي never أثناء التوزيع عندما يكون عضوًا في الاتحاد.📖 ملخص
- النوع الشرطي
T extends U ? X : Yهو عامل ثلاثي على مستوى النوع - الاستدلال: استدلال متغيرات الأنواع في الأنواع الشرطية — استدلال عناصر المصفوفات، وقيم إرجاع الدوال، وقيم تسوية الـ Promise
- نوع تعيين يخلق نوعًا جديدًا استنادًا إلى نوع قديم —
{ [K in keyof T]: T[K] }— يدعم المعدِّلات +/- وإعادة تعيين المفاتيح - القيم الافتراضية للمعلمات العامة
<T = DefaultValue>توفير أنواع بديلة للمعلمات العامة - يصف التباين المشترك (في نفس الاتجاه) والتباين المعاكس (في الاتجاه المعاكس) العلاقات بين الأنواع الفرعية للأنواع العامة — فأنواع عوائد الدوال متباينة بشكل مشترك، في حين أن المعلمات متباينة بشكل معاكس
📝 تمارين
- المشكلة الأساسية (الصعوبة ⭐): قم بتنفيذ
IsArray<T>باستخدام الأنواع الشرطية — إذا كان T نوع مصفوفة، فقم بإرجاع true؛ وإلا، فقم بإرجاع false. اختبرIsArray<string[]>وIsArray<number>. - مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ
Stringify<T>باستخدام نوع التعيين — قم بتحويل أنواع جميع خصائص الكائن إلىstring. على سبيل المثال،{ age: number }→{ age: string }. - مشكلة التحدي (الصعوبة ⭐⭐⭐): استخدم
inferوالأنواع الشرطية لتنفيذDeepPromise<T>— قم بفك تداخل الـ Promises المتداخلة بشكل متكرر حتى يتم الحصول على نوع غير Promise. يجب أن تُرجع حالة الاختبارDeepPromise<Promise<Promise<Promise<number>>>>عددًا.