أنواع TypeScript المتقدمة: سحر الأنواع الشرطية، والمُعيَّنة، وقوالب النصوص الحرفية
غوص عميق في أقوى ميزات الأنواع في TypeScript — الأنواع الشرطية، والأنواع المُعيَّنة، وكلمة infer، وأنواع قوالب النصوص الحرفية. أنماط حقيقية ستستخدمها فعلاً.
معظم مطوري TypeScript يتوقفون عند نفس النقطة. يعرفون الواجهات، والأنواع العامة (Generics)، وأنواع الاتحاد (Union Types). يستطيعون كتابة أنواع لمكوّن React. يستطيعون إزالة التحذيرات الحمراء. لكنهم يتعاملون مع نظام الأنواع كشرّ لا بد منه — شيء موجود لمنع الأخطاء، وليس شيئاً يساعدهم فعلياً في تصميم برمجيات أفضل.
بقيت في تلك المرحلة لحوالي سنتين. ما أخرجني منها هو إدراك أن نظام أنواع TypeScript هو لغة برمجة بحد ذاتها. لديه شروط، وحلقات تكرار، ومطابقة أنماط، وتكرار ذاتي (recursion)، ومعالجة نصوص. بمجرد أن تستوعب ذلك، يتغير كل شيء. تتوقف عن محاربة المُترجم وتبدأ بالتعاون معه.
يغطي هذا المقال ميزات الأنواع على مستوى النوع التي أستخدمها باستمرار في الكود الإنتاجي. ليست تمارين أكاديمية — بل أنماط حقيقية أنقذتني من أخطاء حقيقية.
عقلية البرمجة على مستوى الأنواع#
قبل الغوص في بناء الجمل، أريد أن أُعيد صياغة طريقة تفكيرك في الأنواع.
في البرمجة على مستوى القيم، تكتب دوال تحوّل البيانات في وقت التشغيل:
function extractIds(users: User[]): number[] {
return users.map(u => u.id);
}في البرمجة على مستوى الأنواع، تكتب أنواعاً تحوّل أنواعاً أخرى في وقت الترجمة:
type ExtractIds<T extends { id: unknown }[]> = T[number]["id"];النموذج الذهني هو نفسه — مُدخل، تحويل، مُخرج. الفرق هو أن الكود على مستوى الأنواع يعمل أثناء الترجمة، وليس في وقت التشغيل. لا ينتج أي JavaScript. غرضه الوحيد هو جعل الحالات المستحيلة غير قابلة للتمثيل.
إذا سبق وكتبت دالة حيث ثلاثة من أصل أربعة معاملات لا تكون منطقية إلا في مجموعات معينة، أو حيث نوع القيمة المُرجعة يعتمد على ما تمرره، فأنت بالفعل بحاجة للبرمجة على مستوى الأنواع. ربما لم تكن تعلم أن TypeScript يستطيع التعبير عن ذلك.
الأنواع الشرطية#
الأنواع الشرطية هي if/else نظام الأنواع. بناء الجملة يشبه المُعامل الثلاثي:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
type C = IsString<string>; // trueكلمة extends هنا لا تعني الوراثة. تعني "هل يمكن إسناده إلى". فكّر فيها كـ "هل يتناسب T مع شكل string؟"
بناء أنواع المساعدة الخاصة بك#
لنُعيد بناء بعض أنواع المساعدة المدمجة في TypeScript لنفهم كيف تعمل.
NonNullable — يزيل null و undefined من الاتحاد:
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Test = MyNonNullable<string | null | undefined>;
// Result: stringالنوع never هو "المجموعة الفارغة" — يزيل شيئاً من الاتحاد. عندما يتحول فرع من نوع شرطي إلى never، يختفي ذلك العضو من الاتحاد ببساطة.
Extract — يحتفظ فقط بأعضاء الاتحاد المطابقين للقيد:
type MyExtract<T, U> = T extends U ? T : never;
type Numbers = Extract<string | number | boolean, number | boolean>;
// Result: number | booleanExclude — العكس، يزيل الأعضاء المطابقين:
type MyExclude<T, U> = T extends U ? never : T;
type WithoutStrings = Exclude<string | number | boolean, string>;
// Result: number | booleanالأنواع الشرطية التوزيعية#
هنا تصبح الأمور مثيرة، وحيث يرتبك معظم الناس. عندما تمرر نوع اتحاد إلى نوع شرطي، يوزّع TypeScript الشرط على كل عضو في الاتحاد بشكل فردي.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// You might expect: (string | number)[]
// Actual result: string[] | number[]TypeScript لا يقيّم (string | number) extends unknown. بدلاً من ذلك، يقيّم string extends unknown و number extends unknown بشكل منفصل، ثم يجمع النتائج في اتحاد.
لهذا تعمل Extract و Exclude بهذه الطريقة. التوزيع يحدث تلقائياً عندما يكون T معامل نوع "عارياً" (غير مغلف بشيء).
إذا أردت منع التوزيع، غلّف الجانبين في صفّ (tuple):
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// Result: (string | number)[]لقد وقعت في هذا الفخ أكثر مما أود الاعتراف. إذا كان نوعك الشرطي يعطي نتائج غير متوقعة مع مدخلات الاتحاد، فإن التوزيع هو السبب تقريباً دائماً.
مثال عملي: معالج استجابة API#
إليك نمطاً أستخدمه عند التعامل مع استجابات API التي يمكن أن تنجح أو تفشل:
type ApiResponse<T> =
| { status: "success"; data: T; error: never }
| { status: "error"; data: never; error: string };
type ExtractData<R> = R extends { status: "success"; data: infer D } ? D : never;
type ExtractError<R> = R extends { status: "error"; error: infer E } ? E : never;
type UserResponse = ApiResponse<{ id: number; name: string }>;
type UserData = ExtractData<UserResponse>;
// Result: { id: number; name: string }
type UserError = ExtractError<UserResponse>;
// Result: stringهذا أنظف من كتابة حراس أنواع يدوية لكل شكل استجابة.
كلمة infer#
infer هي مطابقة الأنماط للأنواع. تتيح لك الإعلان عن متغير نوع داخل نوع شرطي يكتشفه TypeScript نيابةً عنك.
فكّر فيها كـ: "أعرف أن هذا النوع له شكل معين. استخرج الجزء الذي يهمني."
استخراج أنواع القيم المُرجعة#
type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
type Fn = (x: number, y: string) => boolean;
type Result = MyReturnType<Fn>; // booleanTypeScript يرى النمط (...args) => something، يطابق دالتك مقابله، ويسند نوع القيمة المُرجعة إلى R.
استخراج أنواع المعاملات#
type MyParameters<T> = T extends (...args: infer P) => unknown ? P : never;
type Fn = (x: number, y: string) => void;
type Params = MyParameters<Fn>; // [x: number, y: string]فك تغليف الوعود (Promises)#
هذا أستخدمه طوال الوقت عند العمل مع الدوال غير المتزامنة:
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnpackPromise<Promise<string>>; // string
type B = UnpackPromise<Promise<number[]>>; // number[]
type C = UnpackPromise<string>; // string (passthrough)للوعود المتداخلة بعمق:
type DeepUnpackPromise<T> = T extends Promise<infer U>
? DeepUnpackPromise<U>
: T;
type Deep = DeepUnpackPromise<Promise<Promise<Promise<number>>>>;
// Result: numberاستخراج أنواع عناصر المصفوفة#
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Items = ElementOf<string[]>; // string
type Mixed = ElementOf<(string | number)[]>; // string | numberاستخدام infer عدة مرات في نوع واحد#
يمكنك استخدام infer عدة مرات لاستخراج أجزاء مختلفة:
type FirstAndLast<T extends unknown[]> =
T extends [infer First, ...unknown[], infer Last]
? { first: First; last: Last }
: never;
type Result = FirstAndLast<[1, 2, 3, 4]>;
// Result: { first: 1; last: 4 }واقعي: استخراج الأحداث بأمان نوعي#
إليك نمطاً أستخدمه في الأنظمة المبنية على الأحداث:
type EventMap = {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"order:created": { orderId: string; total: number };
"order:shipped": { orderId: string; trackingId: string };
};
type EventName = keyof EventMap;
type EventPayload<E extends EventName> = EventMap[E];
// Extract all user-related events
type UserEvents = Extract<EventName, `user:${string}`>;
// Result: "user:login" | "user:logout"
// Get the payload for user events only
type UserEventPayloads = EventPayload<UserEvents>;
// Result: { userId: string; timestamp: number } | { userId: string }الأنواع المُعيَّنة (Mapped Types)#
الأنواع المُعيَّنة تتيح لك إنشاء أنواع كائنات جديدة عبر تحويل كل خاصية في نوع موجود. إنها map() نظام الأنواع.
الأساسيات#
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};keyof T يعطيك اتحاد جميع مفاتيح الخصائص. [K in ...] يتكرر على كل واحد منها. T[K] هو نوع تلك الخاصية.
بناء أنواع المساعدة من الصفر#
Required — جعل جميع الخصائص إلزامية:
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};صيغة -? تزيل المُعدِّل الاختياري. وبالمثل، -readonly تزيل readonly.
Pick — اختيار خصائص محددة:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = MyPick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }Omit — إزالة خصائص محددة:
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
type SafeUser = MyOmit<User, "password">;
// { id: number; name: string; email: string }Record — إنشاء نوع كائن بمفاتيح وأنواع قيم محددة:
type MyRecord<K extends string | number | symbol, V> = {
[P in K]: V;
};
type StatusMap = MyRecord<"active" | "inactive" | "banned", boolean>;
// { active: boolean; inactive: boolean; banned: boolean }إعادة تعيين المفاتيح مع as#
TypeScript 4.1 قدّم إعادة تعيين المفاتيح، الذي يتيح لك تحويل أسماء الخصائص:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
email: string;
}
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }يمكنك أيضاً تصفية الخصائص عبر إعادة التعيين إلى never:
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringProps = OnlyStrings<Person>;
// { name: string; email: string }هذا قوي بشكل لا يُصدق. أنت تتكرر على الخصائص، وتصفيها، وتحوّل أسماءها في الوقت ذاته — كل ذلك على مستوى الأنواع.
مثال عملي: أنواع التحقق من النماذج#
لقد استخدمت هذا النمط في عدة مكتبات نماذج:
interface FormFields {
username: string;
email: string;
age: number;
bio: string;
}
type ValidationErrors<T> = {
[K in keyof T]?: string[];
};
type TouchedFields<T> = {
[K in keyof T]?: boolean;
};
type DirtyFields<T> = {
[K in keyof T]?: boolean;
};
type FormState<T> = {
values: T;
errors: ValidationErrors<T>;
touched: TouchedFields<T>;
dirty: DirtyFields<T>;
isValid: boolean;
isSubmitting: boolean;
};
// Now your form state is fully typed based on the fields
type MyFormState = FormState<FormFields>;أضف حقلاً إلى FormFields، وكل نوع مرتبط يتحدث تلقائياً. احذف حقلاً، والمُترجم يلتقط كل مرجع. هذا هو نوع الأشياء التي تمنع فئات كاملة من الأخطاء.
أنواع قوالب النصوص الحرفية (Template Literal Types)#
أنواع قوالب النصوص الحرفية تتيح لك معالجة النصوص على مستوى الأنواع. تستخدم نفس صيغة الفاصلة العكسية مثل قوالب JavaScript، لكن للأنواع.
معالجة النصوص الأساسية#
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, world"; // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, world"; // Error!TypeScript يوفر أيضاً أنواع معالجة نصوص مدمجة:
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"أنماط أسماء الأحداث#
هنا تتألق أنواع قوالب النصوص الحرفية فعلاً. إليك نمطاً أستخدمه لأنظمة أحداث آمنة نوعياً:
type EventNames<T extends string> = `${T}Changed` | `${T}Deleted` | `${T}Created`;
type ModelEvents = EventNames<"user" | "order" | "product">;
// "userChanged" | "userDeleted" | "userCreated"
// | "orderChanged" | "orderDeleted" | "orderCreated"
// | "productChanged" | "productDeleted" | "productCreated"تسعة أحداث من معاملي نوع. وجميعها آمنة نوعياً.
أنماط CSS في TypeScript#
أنواع قوالب النصوص الحرفية يمكنها فرض أنماط CSS على مستوى الأنواع:
type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}`;
function setWidth(value: CSSValue): void {
// implementation
}
setWidth("100px"); // OK
setWidth("2.5rem"); // OK
setWidth("50%"); // OK
// setWidth("100"); // Error: not a valid CSS value
// setWidth("big"); // Errorتحليل النصوص مع قوالب النصوص الحرفية#
يمكنك استخدام أنواع قوالب النصوص الحرفية مع infer لتحليل النصوص:
type ParseRoute<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ParseRoute<`/${Rest}`>
: T extends `${infer _Start}:${infer Param}`
? Param
: never;
type RouteParams = ParseRoute<"/users/:userId/posts/:postId">;
// Result: "userId" | "postId"هذا هو الأساس الذي تعمل عليه مكتبات التوجيه الآمنة نوعياً مثل tRPC. النص /users/:userId/posts/:postId يُحلَّل على مستوى الأنواع لاستخراج أسماء المعاملات.
دمج قوالب النصوص الحرفية مع الأنواع المُعيَّنة#
هنا تصبح الأمور قوية حقاً:
type EventHandlers<T extends Record<string, unknown>> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
newValue: T[K],
oldValue: T[K]
) => void;
};
interface ThemeConfig {
color: string;
fontSize: number;
darkMode: boolean;
}
type ThemeHandlers = EventHandlers<ThemeConfig>;
// {
// onColorChange: (newValue: string, oldValue: string) => void;
// onFontSizeChange: (newValue: number, oldValue: number) => void;
// onDarkModeChange: (newValue: boolean, oldValue: boolean) => void;
// }من واجهة إعدادات بسيطة، تحصل على معالجات أحداث مُنمّطة بالكامل بأنواع معاملات صحيحة. هذا هو نوع الكود على مستوى الأنواع الذي يوفّر وقت التطوير فعلاً.
متقدم: أنواع مسارات التنقيط (Dot-Notation)#
إليك نمطاً أكثر تقدماً — توليد جميع مسارات التنقيط الممكنة عبر كائن متداخل:
type DotPaths<T, Prefix extends string = ""> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? `${Prefix}${K}` | DotPaths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string]
: never;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
server: {
port: number;
};
}
type ConfigPaths = DotPaths<Config>;
// "database" | "database.host" | "database.port"
// | "database.credentials" | "database.credentials.username"
// | "database.credentials.password" | "server" | "server.port"الآن يمكنك كتابة دالة getConfig تُكمل تلقائياً كل مسار ممكن وتُرجع النوع الصحيح لكل منها:
type GetValueByPath<T, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? GetValueByPath<T[Key], Rest>
: never
: P extends keyof T
? T[P]
: never;
function getConfig<P extends ConfigPaths>(path: P): GetValueByPath<Config, P> {
// implementation
const keys = path.split(".");
let result: unknown = config;
for (const key of keys) {
result = (result as Record<string, unknown>)[key];
}
return result as GetValueByPath<Config, P>;
}
const host = getConfig("database.host"); // type: string
const port = getConfig("database.port"); // type: number
const username = getConfig("database.credentials.username"); // type: stringإكمال تلقائي كامل، أمان نوعي كامل، صفر حمل إضافي في وقت التشغيل من الأنواع.
الأنواع العودية (Recursive Types)#
TypeScript يدعم تعريفات الأنواع العودية، مما يتيح لك التعامل مع هياكل متداخلة بعمق عشوائي.
Deep Partial#
Partial<T> المدمج يجعل فقط خصائص المستوى الأعلى اختيارية. للكائنات المتداخلة، تحتاج للعودية:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Array<infer U>
? Array<DeepPartial<U>>
: DeepPartial<T[K]>
: T[K];
};
interface Settings {
theme: {
colors: {
primary: string;
secondary: string;
};
fonts: {
heading: string;
body: string;
};
};
notifications: {
email: boolean;
push: boolean;
};
}
type PartialSettings = DeepPartial<Settings>;
// Every nested property is now optionalلاحظ فحص المصفوفة: بدونه، ستُعامَل المصفوفات ككائنات وستصبح فهارسها الرقمية اختيارية، وهذا ليس ما تريده.
Deep Readonly#
نفس النمط، مُعدِّل مختلف:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: T[K] extends Array<infer U>
? ReadonlyArray<DeepReadonly<U>>
: DeepReadonly<T[K]>
: T[K];
};فحص Function مهم — بدونه، ستُطبَّق readonly على البنية الداخلية لخصائص الدوال، وهذا لا معنى له.
نوع JSON#
إليك نوعاً عودياً كلاسيكياً — تمثيل أي قيمة JSON صالحة:
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonValue[];
type JsonObject = { [key: string]: JsonValue };
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
function parseJson(input: string): JsonValue {
return JSON.parse(input);
}
function isJsonObject(value: JsonValue): value is JsonObject {
return typeof value === "object" && value !== null && !Array.isArray(value);
}هذا أفضل بكثير من استخدام any لتحليل JSON. يخبر المستهلك "ستحصل على شيء ما، لكنك تحتاج لتضييقه قبل استخدامه." وهذا هو الواقع بالضبط.
نوع عودي لقوائم التنقل المتداخلة#
مثال عملي — كتابة أنواع لهيكل قائمة تنقل:
interface MenuItem {
label: string;
href: string;
icon?: string;
children?: MenuItem[];
}
type Menu = MenuItem[];
const navigation: Menu = [
{
label: "Products",
href: "/products",
children: [
{
label: "Software",
href: "/products/software",
children: [
{ label: "IDE", href: "/products/software/ide" },
{ label: "CLI Tools", href: "/products/software/cli" },
],
},
{ label: "Hardware", href: "/products/hardware" },
],
},
{ label: "About", href: "/about" },
];خاصية children العودية تمنحك تداخلاً لا نهائياً مع أمان نوعي كامل.
أنماط من العالم الحقيقي#
دعني أشارك بعض الأنماط التي استخدمتها فعلاً في أنظمة إنتاجية.
نمط البنّاء مع الأنواع (Builder Pattern)#
نمط البنّاء يصبح أكثر فائدة بشكل ملحوظ عندما يتتبع نظام الأنواع ما تم تعيينه:
type RequiredKeys = "host" | "port" | "database";
type BuilderState = {
[K in RequiredKeys]?: true;
};
class DatabaseConfigBuilder<State extends BuilderState = {}> {
private config: Partial<{
host: string;
port: number;
database: string;
ssl: boolean;
poolSize: number;
}> = {};
setHost(host: string): DatabaseConfigBuilder<State & { host: true }> {
this.config.host = host;
return this as unknown as DatabaseConfigBuilder<State & { host: true }>;
}
setPort(port: number): DatabaseConfigBuilder<State & { port: true }> {
this.config.port = port;
return this as unknown as DatabaseConfigBuilder<State & { port: true }>;
}
setDatabase(db: string): DatabaseConfigBuilder<State & { database: true }> {
this.config.database = db;
return this as unknown as DatabaseConfigBuilder<State & { database: true }>;
}
setSsl(ssl: boolean): DatabaseConfigBuilder<State> {
this.config.ssl = ssl;
return this as unknown as DatabaseConfigBuilder<State>;
}
setPoolSize(size: number): DatabaseConfigBuilder<State> {
this.config.poolSize = size;
return this as unknown as DatabaseConfigBuilder<State>;
}
build(
this: DatabaseConfigBuilder<{ host: true; port: true; database: true }>
): Required<Pick<typeof this.config, "host" | "port" | "database">> &
Partial<Pick<typeof this.config, "ssl" | "poolSize">> {
return this.config as ReturnType<typeof this.build>;
}
}
// This compiles:
const config = new DatabaseConfigBuilder()
.setHost("localhost")
.setPort(5432)
.setDatabase("myapp")
.setSsl(true)
.build();
// This fails at compile time — missing .setDatabase():
// const bad = new DatabaseConfigBuilder()
// .setHost("localhost")
// .setPort(5432)
// .build(); // Error: 'database' is missingدالة build() تصبح قابلة للاستدعاء فقط عندما يتم تعيين جميع الحقول المطلوبة. يُلتقط هذا في وقت الترجمة، وليس في وقت التشغيل.
باعث أحداث آمن نوعياً (Type-Safe Event Emitter)#
إليك باعث أحداث حيث تُفرض أنواع الحمولة:
type EventMap = Record<string, unknown>;
class TypedEventEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: Array<(payload: Events[K]) => void>;
} = {};
on<K extends keyof Events>(
event: K,
listener: (payload: Events[K]) => void
): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.listeners[event]?.forEach((listener) => listener(payload));
}
off<K extends keyof Events>(
event: K,
listener: (payload: Events[K]) => void
): void {
const handlers = this.listeners[event];
if (handlers) {
this.listeners[event] = handlers.filter((h) => h !== listener) as typeof handlers;
}
}
}
// Usage
interface AppEvents {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"notification:new": { message: string; level: "info" | "warning" | "error" };
}
const emitter = new TypedEventEmitter<AppEvents>();
// Fully typed — IDE autocompletes event names and payload shapes
emitter.on("user:login", (payload) => {
console.log(payload.userId); // string
console.log(payload.timestamp); // number
});
// Type error: payload doesn't match
// emitter.emit("user:login", { userId: "123" });
// Error: missing 'timestamp'
// Type error: event doesn't exist
// emitter.on("user:signup", () => {});
// Error: "user:signup" is not in AppEventsلقد استخدمت هذا النمط في ثلاثة مشاريع إنتاجية مختلفة. إنه يلتقط فئات كاملة من الأخطاء المتعلقة بالأحداث في وقت الترجمة.
الاتحادات المُميَّزة مع الفحص الشامل#
الاتحادات المُميَّزة هي على الأرجح النمط الأكثر فائدة في TypeScript. مع الفحص الشامل، تضمن أنك تتعامل مع كل حالة:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
default:
// This line ensures exhaustive checking
const _exhaustive: never = shape;
return _exhaustive;
}
}إذا أضاف شخص ما متغيراً جديداً للشكل (مثل "pentagon")، ستفشل هذه الدالة في الترجمة لأنه لا يمكن إسناد قيمة لـ never. المُترجم يجبرك على التعامل مع كل حالة.
أطوّر هذا أكثر بدالة مساعدة:
function assertNever(value: never, message?: string): never {
throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
default:
assertNever(shape, `Unknown shape kind: ${(shape as Shape).kind}`);
}
}آلات الحالة الآمنة نوعياً#
الاتحادات المُميَّزة تنمذج آلات الحالة بشكل جميل أيضاً:
type ConnectionState =
| { status: "disconnected" }
| { status: "connecting"; attempt: number }
| { status: "connected"; socket: WebSocket; connectedAt: Date }
| { status: "error"; error: Error; lastAttempt: Date };
type ConnectionAction =
| { type: "CONNECT" }
| { type: "CONNECTED"; socket: WebSocket }
| { type: "DISCONNECT" }
| { type: "ERROR"; error: Error }
| { type: "RETRY" };
function connectionReducer(
state: ConnectionState,
action: ConnectionAction
): ConnectionState {
switch (state.status) {
case "disconnected":
if (action.type === "CONNECT") {
return { status: "connecting", attempt: 1 };
}
return state;
case "connecting":
if (action.type === "CONNECTED") {
return {
status: "connected",
socket: action.socket,
connectedAt: new Date(),
};
}
if (action.type === "ERROR") {
return {
status: "error",
error: action.error,
lastAttempt: new Date(),
};
}
return state;
case "connected":
if (action.type === "DISCONNECT") {
state.socket.close();
return { status: "disconnected" };
}
if (action.type === "ERROR") {
return {
status: "error",
error: action.error,
lastAttempt: new Date(),
};
}
return state;
case "error":
if (action.type === "RETRY") {
return { status: "connecting", attempt: 1 };
}
if (action.type === "DISCONNECT") {
return { status: "disconnected" };
}
return state;
default:
assertNever(state, "Unknown connection state");
}
}كل حالة لديها فقط الخصائص المنطقية لتلك الحالة. لا يمكنك الوصول إلى socket عندما تكون غير متصل. لا يمكنك الوصول إلى error عندما تكون متصلاً. نظام الأنواع يفرض قيود آلة الحالة.
الأنواع ذات العلامة التجارية لسلامة المجال (Branded Types)#
نمط آخر أجده أساسياً — استخدام الأنواع ذات العلامة التجارية لمنع خلط القيم التي تشترك في نفس النوع الأساسي:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
function getUser(id: UserId): Promise<User> {
// ...
}
function getOrder(id: OrderId): Promise<Order> {
// ...
}
const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
getUser(userId); // OK
// getUser(orderId); // Error! OrderId is not assignable to UserIdكل من UserId و OrderId هما نصوص في وقت التشغيل. لكن في وقت الترجمة، هما نوعان مميزان. لا يمكنك حرفياً تمرير معرّف طلب حيث يُتوقع معرّف مستخدم. هذا التقط أخطاء حقيقية في كل مشروع استخدمته فيه.
المزالق الشائعة#
الأنواع المتقدمة قوية، لكنها تأتي مع فخاخ. إليك ما تعلمته بالطريقة الصعبة.
حدود المراجع الدائرية#
TypeScript لديه حد لعمق العودية للأنواع (حالياً حوالي 50 مستوى، رغم أنه يختلف). إذا تعمقت كثيراً، ستحصل على خطأ "Type instantiation is excessively deep and possibly infinite" المخيف.
// This will hit the recursion limit for deeply nested objects
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// Fix: Add a depth counter
type DeepPartialSafe<T, Depth extends number[] = []> =
Depth["length"] extends 10
? T
: {
[K in keyof T]?: T[K] extends object
? DeepPartialSafe<T[K], [...Depth, 0]>
: T[K];
};حيلة عداد العمق تستخدم صفاً (tuple) ينمو مع كل خطوة عودية. عندما يصل إلى حدّك، تتوقف العودية.
تأثيرات الأداء#
الأنواع المعقدة يمكن أن تبطئ محررك وأوقات البناء بشكل ملحوظ. لقد رأيت مشاريع حيث أضاف نوع واحد مبالغ في الذكاء 3 ثوانٍ إلى حلقة ردود الفعل مع كل ضغطة مفتاح.
علامات التحذير:
- محرر الكود يستغرق أكثر من ثانيتين لإظهار الإكمال التلقائي
tsc --noEmitيستغرق وقتاً أطول بشكل ملحوظ بعد إضافة نوع- ترى أخطاء "Type instantiation is excessively deep"
// This is too clever — it generates a combinatorial explosion
type AllCombinations<T extends string[]> = T extends [
infer First extends string,
...infer Rest extends string[]
]
? `${First}${AllCombinations<Rest>}` | AllCombinations<Rest>
: "";
// Don't do this with more than 5-6 elementsمتى لا تستخدم الأنواع المتقدمة#
ربما هذا القسم هو الأهم. الأنواع المتقدمة يجب أن تُستخدم عندما تمنع أخطاء أو تحسّن تجربة المطور. لا يجب أن تُستخدم للاستعراض.
لا تستخدمها عندما:
Record<string, unknown>بسيط سيكون كافياً- النوع يُستخدم في مكان واحد فقط ونوع محدد سيكون أوضح
- تقضي وقتاً في تصحيح النوع أكثر مما سيوفره
- فريقك لا يستطيع قراءته أو صيانته
- فحص وقت التشغيل سيكون أنسب
استخدمها عندما:
- لديك نمط يتكرر عبر أنواع كثيرة (الأنواع المُعيَّنة)
- نوع القيمة المُرجعة لدالة يعتمد على مدخلاتها (الأنواع الشرطية)
- تبني واجهة مكتبة تحتاج أن تكون موثقة ذاتياً
- تريد جعل الحالات غير المشروعة غير قابلة للتمثيل
- سئمت من نفس فئة الأخطاء التي تظهر في مراجعات الكود
// Over-engineered — don't do this for a simple config
type Config = DeepReadonly<
DeepRequired<
Merge<DefaultConfig, Partial<UserConfig>>
>
>;
// Just do this
interface Config {
readonly host: string;
readonly port: number;
readonly debug: boolean;
}قاعدتي العامة: إذا كان شرح النوع يستغرق وقتاً أطول من شرح الخطأ الذي يمنعه، بسّطه.
تصحيح الأنواع المعقدة#
عندما لا يعمل نوع معقد، أستخدم هذا المساعد لـ "رؤية" ما حلّه TypeScript:
// Expands a type for inspection in IDE hover tooltips
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
// Use it to debug
type Problematic = SomeComplexType<Input>;
type Debug = Prettify<Problematic>;
// Now hover over Debug in your IDE to see the resolved typeحيلة & {} تجبر TypeScript على تقييم النوع بشغف بدلاً من إظهار اسم النوع المستعار. لقد وفّرت عليّ ساعات من الارتباك.
تقنية أخرى — عزل واختبار تدريجياً:
// Instead of debugging this all at once:
type Final = StepThree<StepTwo<StepOne<Input>>>;
// Break it apart:
type AfterStep1 = StepOne<Input>; // hover to check
type AfterStep2 = StepTwo<AfterStep1>; // hover to check
type AfterStep3 = StepThree<AfterStep2>; // hover to checkخلاصة#
- الأنواع الشرطية (
T extends U ? X : Y) هي if/else للأنواع. انتبه للسلوك التوزيعي مع الاتحادات. inferهي مطابقة أنماط — استخدمها لاستخراج الأنواع من هياكل مثل توقيعات الدوال والوعود والمصفوفات.- الأنواع المُعيَّنة (
{ [K in keyof T]: ... }) تتكرر على الخصائص. ادمجها معasلإعادة تعيين المفاتيح والتصفية. - أنواع قوالب النصوص الحرفية تعالج النصوص على مستوى الأنواع. مدمجة مع الأنواع المُعيَّنة، فهي قوية بشكل لا يُصدق لتصميم الواجهات.
- الأنواع العودية تتعامل مع الهياكل المتداخلة لكنها تحتاج حدود عمق لتجنب انفجارات المُترجم.
- الاتحادات المُميَّزة مع الفحص الشامل هي النمط الأعلى قيمة في TypeScript. استخدمها في كل مكان.
- الأنواع ذات العلامة التجارية تمنع خلط القيم التي تشترك في نفس النوع الأساسي. بسيطة التنفيذ، عالية التأثير.
- لا تبالغ في هندسة الأنواع. إذا كان فهم النوع أصعب من فهم الخطأ الذي يمنعه، بسّطه. الهدف هو جعل قاعدة الكود أكثر أماناً، وليس الفوز بمسابقة غولف الأنواع.
نظام أنواع TypeScript مكتمل بتورينغ، مما يعني أنك تستطيع فعل أي شيء تقريباً به. الفن يكمن في معرفة متى يجب عليك ذلك.