TypeScript الحديثة: أنماط وممارسات لعام 2026
من الإعدادات الصارمة إلى عملاء API الآمنين كتابة — أنماط TypeScript التي تفصل كود الإنتاج عن المشاريع الترفيهية في 2026.
تطور TypeScript أسرع مما تستطيع معظم النظم البيئية مواكبته. كل إصدار يجلب بناء جملة جديداً، وفحوصات أكثر صرامة، وأنماطاً تعيد كتابة ما يبدو أصيلاً. الفرق بين كود TypeScript الذي يترجم فقط والكود الذي يستفيد حقاً من نظام الكتابة شاسع — وهذه الفجوة تحدد ما إذا كانت أنواعك توثيقاً أم ضوضاء.
هذا المقال يغطي الأنماط الأكثر أهمية لـ TypeScript الإنتاجي في 2026. هذه ليست تمارين أكاديمية. إنها الممارسات التي تجعل قواعد الكود الكبيرة أكثر أماناً، وواجهات API أصعب في الإساءة الاستخدام، وإعادة الهيكلة أقل رعباً. كل مثال يأتي من أنماط حقيقية مستخدمة في أنظمة إنتاجية تتعامل مع ملايين الطلبات.
الأساس: إعداد TypeScript لعام 2026
القرار الأكثر تأثيراً تتخذه بشأن جودة TypeScript ليس في كودك — بل في ملف tsconfig.json. المستوى الأساسي تجاوز strict: true. في 2026، إعداد الإنتاج يجب أن يفعل فحوصات كانت اختيارية أو تجريبية في الإصدارات السابقة.
// tsconfig.json — the 2026 baseline
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}كل علامة تلغي فئة من الأخطاء. exactOptionalPropertyTypes تمنع الخطأ الشائع حيث يتم تعيين ?: string | undefined صراحة إلى undefined — التمييز بين المفقود والموجود ولكن غير محدد مهم في حدود API. noUncheckedIndexedAccess تجبرك على التعامل مع undefined لكل وصول كائن بمفتاح ديناميكي، مما يمسك أعطال وقت التشغيل قبل نشرها. verbatimModuleSyntax تضمن أن دقة الوحدة تتطابق مع وقت التشغيل، مما يلغي عدم التطابق الصامت الذي يكسر مشاريع ESM في الإنتاج.
الفرق التي تتبنى هذا الإعداد تبلغ عن حوادث إنتاجية أقل بشكل كبير تتعلق بمراجع null وخصائص undefined. عبء التعامل مع تلك الفحوصات الإضافية لـ undefined مسبقاً تافه مقارنة بتصحيح عطل لأن استجابة API كانت تفتقد حقلاً افترضت وجوده.
الاتحادات المميزة — أقوى نمط لديك
الاتحادات المميزة هي النمط الأكثر تأثيراً في TypeScript. إنها تمثل الحالات بشكل صريح، وتجعل الحالات غير القانونية غير قابلة للتمثيل، وتعطي المترجم المعلومات التي يحتاجها لفرض المعالجة الشاملة. إذا لم تكن تستخدمها لأي بيانات لها أشكال متعددة، فأنت تقاتل نظام الكتابة بدلاً من تركه يعمل لصالحك.
type ApiState<S, E = Error> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: S }
| { status: "error"; error: E };
// Exhaustive match — if you add a status, this breaks at compile time
function renderState<S>(state: ApiState<S>): string {
switch (state.status) {
case "idle":
return "Awaiting input";
case "loading":
return "Loading...";
case "success":
return `Got ${JSON.stringify(state.data)}`;
case "error":
return `Failed: ${state.error.message}`;
}
}
// Usage — impossible to access data on a loading state
const userState: ApiState<User> = { status: "loading" };
// userState.data — does not compile
// userState.status — narrows correctly after any checkالسحر في التضييق. عندما تتحقق state.status === "success"، يعرف TypeScript بالضبط أي متغير أنت فيه ويوفر النوع الصحيح لكل حقل في ذلك الفرع. هذا يلغي فئات كاملة من الفحوصات الدفاعية التي قد تحتاجها كود وقت التشغيل. يصبح المترجم مجموعة الاختبارات الخاصة بك لانتقالات الحالة غير الصالحة.
لكي يعمل هذا النمط بفعالية، يجب أن تكون خاصية التمييز (status في المثال أعلاه) من نوع حرفي، وليس سلسلة عامة. كل متغير يجب أن يكون له قيمة حرفية فريدة. يستخدم المترجم تلك القيمة الحرفية للتمييز بين الفروع، وسيحذرك إذا كان لمتغيرين نفس قيمة التمييز.
- استخدم دائماً سلسلة حرفية أو رقماً كأداة تمييز — أبداً نوع سلسلة عام.
- اجعل الخصائص المشتركة ضئيلة. كل ما يختلف بين المتغيرات ينتمي داخل كائن المتغير.
- اجمع مع النوع never لفحوصات الشمولية: أعلن متغيراً من النوع never في الفرع الافتراضي من switch لاصطياد الحالات غير المعالجة في وقت الترجمة.
أنواع القوالب الحرفية والأنواع الموسومة
أنواع القوالب الحرفية والأنواع الموسومة تحل مشكلتين متميزتين لا يمكن لأنواع TypeScript العادية معالجتهما. أنواع القوالب الحرفية تعطيك التحقق من صحة السلسلة على مستوى النوع. الأنواع الموسومة تعطيك كتابة اسمية في عالم كتابة هيكلي — تتيح لك التمييز بين قيمتين لهما نفس الشكل لكن معاني دلالية مختلفة.
// Template literal type — valid API routes are checked at compile time
type ApiRoute = `/api/${string}`;
type UserRoute = `/api/users/${string}`;
function fetchApi<T>(route: ApiRoute): Promise<T> {
return fetch(route).then((r) => r.json());
}
// fetchApi("/invalid"); // Error: not assignable
// fetchApi("/api/users/123"); // OK
// Branded type — distinguish IDs that are both strings
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId): Promise<User> {
return db.users.find(id);
}
const orderId = "ord_123" as OrderId;
// getUser(orderId); // Error: Type 'OrderId' is not assignable to type 'UserId'أنواع القوالب الحرفية تتألق في أي نظام حيث تتبع السلاسل تنسيقاً يمكن التنبؤ به. بناة مسارات API، ومولدي فئات CSS، ومطابقات مفاتيح i18n، وأنظمة أسماء الأحداث كلها تستفيد من التحقق من صحة قيم السلاسل في وقت الترجمة. البناء البديهي — تكتب نمطاً مع عناصر نائبة ${}، ويتحقق TypeScript من أن القيم الفعلية تطابق ذلك النمط.
الأنواع الموسومة تحل مشكلة مختلفة. TypeScript مكتوب بشكل هيكلي، مما يعني أن نوعين لهما نفس الشكل قابلان للتبادل. هذا عادةً ميزة، لكنه يصبح خطراً عندما يكون لديك معرفات كلاهما سلاسل لكنهما يمثلان كيانات مختلفة. UserId وOrderId لا يجب أن يكونا قابلين للتبادل، حتى لو كان كلاهما سلاسل. التقاطع مع { __brand: "X" } يخلق نوعاً وهمياً موجوداً فقط في وقت الترجمة — ليس له تكلفة في وقت التشغيل.
الأنواع الموسومة هي أقرب ما يصل إليه TypeScript من الكتابة الاسمية دون أدوات إضافية. تقاطع واحد مع خاصية وهمية يمكن أن يمنع فئة الأخطاء حيث يُمرر المعرف الخطأ إلى الدالة الخطأ.
عامل satisfies — استنتاج بدون تضحية
قبل satisfies، واجه مطورو TypeScript معضلة عند تعريف ثوابت تحتاج إلى مطابقة نوع مع الحفاظ على قيمها الحرفية. يمكنك إما إضافة تعليق نوعي وتفقد الاستنتاج الضيق، أو حذف التعليق وتفقد التحقق من الصحة. عامل satisfies يزيل هذه المقايضة تماماً.
type ColorPalette = {
primary: string;
secondary: string;
accent: string;
};
// Before satisfies — loses literal types
const paletteOld: ColorPalette = {
primary: "#0f0f0f", // type is string, not "#0f0f0f"
secondary: "#ffffff",
accent: "#0055ff",
};
// After satisfies — validates shape, keeps literals
const palette = {
primary: "#0f0f0f", // type is "#0f0f0f"
secondary: "#ffffff",
accent: "#0055ff",
} satisfies ColorPalette;
// palette.primary; // type is "#0f0f0f", not string
// Works with complex types too
type EventMap = {
click: { x: number; y: number };
focus: { target: string };
input: { value: string };
};
const handlers = {
click: (e: { x: number; y: number }) => console.log(e.x, e.y),
focus: (e: { target: string }) => console.log(e.target),
} satisfies Partial<Record<keyof EventMap, Function>>;
// handlers.click — retains the parameter type from the implementation
// handlers.input — missing, but satisfies allows Partial
// Add input handler later without breaking anythingعامل satisfies هو الأكثر قيمة في كائنات التكوين، ومعالجي الأحداث، وهياكل التعيين حيث تريد أمان النوع على الشكل لكنك تحتاج إلى أضيق الأنواع الممكنة للقيم. إنه يستبدل مجموعة من الحلول البديلة — تأكيدات as const مع تعليقات نوعية، وإعادة كتابة نوع زائدة، ودوال تحقق منفصلة — بكلمة مفتاحية واحدة.
أنماط الأدوية العامة وعملاء API الآمنين كتابة
الأدوية العامة في TypeScript سهلة الاستخدام للحالات البسيطة — Array<T>، Promise<T> — لكنها تصبح قوية حقاً عندما تجمع بين القيود والأنواع الشرطية والاستنتاج في توقيع دالة واحدة. التطبيق الأكثر عملية للأدوية العامة المتقدمة هو بناء عملاء API آمنين كتابة يلغي فئات كاملة من أخطاء وقت التشغيل.
// Type-safe API client
import { z } from "zod";
// Infer the output type from a Zod schema
type InferSchema<T extends z.ZodTypeAny> = T["_output"];
// Route definitions with path params and response schema
type RouteDef = {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
params?: Record<string, string>;
body?: unknown;
};
class ApiClient {
constructor(private base: string) {}
get<
TPath extends string,
TSchema extends z.ZodTypeAny,
>(
path: TPath,
schema: TSchema,
...[params]: TPath extends `${string}:${infer _}/${string}`
? [Record<string, string>]
: TPath extends `${string}:${infer _}`
? [Record<string, string>]
: [Record<string, string>?]
): Promise<InferSchema<TSchema>> {
const resolved = params
? Object.entries(params).reduce(
(p, [k, v]) => p.replace(`:${k}`, v),
path
)
: path;
return fetch(`${this.base}${resolved}`)
.then((r) => r.json())
.then((d) => schema.parse(d) as InferSchema<TSchema>);
}
}
const api = new ApiClient("https://api.example.com");
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// api.get("/users/:id", userSchema, { id: "123" });
// Result type: { id: string; name: string; email: string }
// If params are required but missing, type error at compile timeيجمع نمط ApiClient بين عدة تقنيات متقدمة. الأنواع الشرطية مع infer تكتشف ما إذا كان المسار يحتوي على معاملات مسار وتطلب وسيطات params بشكل شرطي. يتم تحليل مخطط الاستجابة في وقت التشغيل مع Zod واستنتاجه في وقت الترجمة، لذا يكون نوع الإرجاع صحيحاً دائماً. إذا غيرت API شكل استجابتها، تحديث المخطط والمترجم يجد كل مستهلك ينكسر.
هذا النمط يتوسع لمئات النقاط النهائية دون مراسم. كل نقطة نهاية هي مجرد سلسلة مسار ومخطط Zod. البنية التحتية العامة تتعامل مع الباقي — تحليل معاملات المسار، والتحقق من صحة الاستجابة، واستنتاج النوع. الفرق التي تستخدم هذا النمط تبلغ عن القضاء على غالبية أخطاء التكامل بين الواجهة الأمامية والخلفية.
- اجمع بين مخططات Zod أو ArkType مع أغلفة fetch عامة لأمان نوع شامل عبر حدود الشبكة.
- استخدم الأنواع الشرطية مع infer لجعل الوسيطات مطلوبة أو اختيارية بناءً على هيكل المسار.
- أعد أنواعاً موسومة من عميل API الخاص بك حتى لا يتمكن المتصلون من تبديل المعرفات عن طريق الخطأ بين أنواع الكيانات المختلفة.
أنماط معالجة الأخطاء وتوسيع الوحدات
معالجة الأخطاء هي المكان الذي تتراجع فيه معظم قواعد كود TypeScript إلى any وتتظاهر بأن المشكلة غير موجودة. نمط Result — المستوحى من Rust والبرمجة الوظيفية — يجلب الأخطاء إلى نظام الكتابة بحيث يفرض المترجم معالجة كل مسار خطأ. توسيع الوحدات يوسع هذا النهج للأنواع الخارجية، مما يتيح لك إضافة أمان نوعي للمكتبات التي تشحن تعريفات نوع غير كاملة أو فضفاضة جداً.
// Result type — errors are part of the return type, not thrown
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function fetchUser(id: UserId): Promise<Result<User, ApiError>> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return { ok: false, error: await ApiError.fromResponse(res) };
}
return { ok: true, value: await res.json() };
} catch (err) {
return { ok: false, error: new ApiError("network", String(err)) };
}
}
// Consumer must handle both branches — compiler enforces it
const result = await fetchUser(userId);
if (result.ok) {
console.log(result.value.name); // result.value is User
} else {
console.error(result.error.code); // result.error is ApiError
}
// --- Module augmentation for third-party types ---
// express-session types are incomplete — augment them
declare module "express-session" {
interface SessionData {
userId?: string;
role: "admin" | "user" | "viewer";
permissions: string[];
}
}
// Now req.session.role narrows to "admin" | "user" | "viewer"
// Without augmentation, req.session.role would be anyنمط Result يفرض معالجة الأخطاء الصريحة في كل موقع استدعاء. لا توجد طريقة لتجاهل عملية فاشلة بصمت — المترجم يطلب منك التحقق من result.ok قبل الوصول إلى result.value. هذا يلغي try-catch المنسي الذي هو مصدر حوادث إنتاجية لا تعد ولا تحصى. الثمن هو كتابة أكثر قليلاً في كل موقع استدعاء، لكن الفائدة هي أنه لا يوجد خطأ يذهب دون معالجة.
توسيع الوحدات يملأ الفجوات حيث تقصر تعريفات النوع الخارجية. العديد من المكتبات الشائعة تشحن بأنواع متساهلة جداً — دوال ترجع any، معاملات مكتوبة كـ object، أو خصائص مفقودة على واجهاتها. بدلاً من إعادة الكتابة بـ as أو استخدام @ts-ignore، استخدم declare module "اسم-المكتبة" في ملف .d.ts أو .ts وأضف الأنواع المفقودة. التصريحات تندمج تلقائياً، وقاعدة الكود بالكامل تستفيد من الإصلاح.
مزيج هذه الأنماط — الإعداد الصارم، الاتحادات المميزة، أنواع القوالب الحرفية، الأنواع الموسومة، satisfies، الأدوية العامة الآمنة، ومعالجة الأخطاء الصريحة — يمثل نهجاً شاملاً لـ TypeScript في 2026. كل نمط مفيد بشكل مستقل، لكن معاً يخلقون قاعدة كود حيث يلتقط المترجم الأخطاء التي كانت لتصبح حوادث إنتاجية. الاستثمار في تعلم هذه الأنماط يدفع ثمنه أضعافاً مضاعفة في تقليل وقت التصحيح، وإعادة هيكلة أكثر أماناً، وواجهات API صعبة الإساءة الاستخدام حقاً.
