لغة TypeScript

أنماط أنواع الأدوات المساعدة المتقدمة

32 دقيقة الدرس 33 من 40

أنماط أنواع الأدوات المساعدة المتقدمة

أنواع الأدوات المساعدة في TypeScript هي كتل بناء قوية، لكن السحر الحقيقي يحدث عندما تجمعها وتوسعها لإنشاء تحويلات نوع متطورة مخصصة. في هذا الدرس، سنستكشف أنماط أنواع الأدوات المساعدة المتقدمة بما في ذلك DeepPartial و DeepReadonly و PathKeys للوصول إلى الكائنات المتداخلة، وتقنيات بناء أدوات نوع قابلة لإعادة الاستخدام تحل تحديات الكتابة المعقدة.

لماذا نبني أنواع أدوات مساعدة مخصصة؟

أنواع الأدوات المساعدة المخصصة تمكنك من:

  • تقليل التكرار: عرّف تحويلات النوع المعقدة مرة واحدة وأعد استخدامها في جميع أنحاء قاعدة الكود الخاصة بك
  • فرض الاتساق: وحّد كيفية تحويل الأنواع عبر تطبيقك
  • تحسين قابلية الصيانة: مركز منطق النوع في أدوات مساعدة قابلة لإعادة الاستخدام بدلاً من تكرارها
  • التعبير عن النية: أنشئ أسماء أنواع موثقة ذاتيًا تنقل الغرض بوضوح
  • معالجة الحالات الخاصة: ابنِ أدوات مساعدة تعالج بشكل صحيح البنيات المتداخلة المعقدة

DeepPartial - خصائص اختيارية متكررة

النوع المدمج Partial<T> يجعل فقط الخصائص على المستوى الأعلى اختيارية. DeepPartial يجعل بشكل متكرر جميع الخصائص المتداخلة اختيارية:

تنفيذ DeepPartial:
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepPartial<U>>
      : DeepPartial<T[P]>
    : T[P];
};

// مثال الاستخدام
interface User {
  id: number;
  profile: {
    firstName: string;
    lastName: string;
    address: {
      street: string;
      city: string;
      country: string;
    };
  };
  preferences: {
    theme: 'light' | 'dark';
    notifications: {
      email: boolean;
      push: boolean;
    };
  };
}

// جميع الخصائص على جميع المستويات تصبح اختيارية
type PartialUser = DeepPartial<User>;

const updateUser: PartialUser = {
  profile: {
    address: {
      city: 'نيويورك', // يمكن تحديث خاصية متداخلة واحدة فقط
    },
  },
};
ملاحظة: DeepPartial مفيد بشكل خاص لعمليات التحديث حيث تريد السماح بتحديثات جزئية للكائنات المتداخلة بعمق دون طلب جميع الخصائص على كل مستوى.

DeepReadonly - عدم التغيير المتكرر

اجعل جميع الخصائص على جميع مستويات التداخل للقراءة فقط لفرض عدم التغيير الكامل:

تنفيذ DeepReadonly:
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Array<infer U>
      ? ReadonlyArray<DeepReadonly<U>>
      : DeepReadonly<T[P]>
    : T[P];
};

// مثال الاستخدام
interface Config {
  api: {
    baseURL: string;
    timeout: number;
    headers: {
      authorization: string;
      contentType: string;
    };
  };
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
}

type ImmutableConfig = DeepReadonly<Config>;

const config: ImmutableConfig = {
  api: {
    baseURL: 'https://api.example.com',
    timeout: 5000,
    headers: {
      authorization: 'Bearer token',
      contentType: 'application/json',
    },
  },
  features: {
    darkMode: true,
    analytics: false,
  },
};

// خطأ TypeScript: لا يمكن التعيين إلى 'baseURL' لأنها خاصية للقراءة فقط
// config.api.baseURL = 'https://new-api.com';

// خطأ TypeScript: لا يمكن التعيين إلى 'darkMode' لأنها خاصية للقراءة فقط
// config.features.darkMode = false;

DeepRequired - خصائص مطلوبة متكررة

اجعل جميع الخصائص الاختيارية مطلوبة على جميع مستويات التداخل:

تنفيذ DeepRequired:
type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepRequired<U>>
      : DeepRequired<T[P]>
    : T[P];
};

// مثال مع خصائص متداخلة اختيارية
interface PartialForm {
  username?: string;
  profile?: {
    bio?: string;
    social?: {
      twitter?: string;
      github?: string;
    };
  };
}

type CompleteForm = DeepRequired<PartialForm>;

// خطأ TypeScript: يجب توفير جميع الخصائص
const form: CompleteForm = {
  username: 'john_doe',
  profile: {
    bio: 'مطور',
    social: {
      twitter: '@johndoe',
      github: 'johndoe',
    },
  },
};
نصيحة: المعدّل -? يزيل العلامة الاختيارية من الخصائص. هذا عكس ? الذي يضيف العلامة الاختيارية.

PathKeys - الوصول الآمن من حيث النوع لمسار الكائن

ولّد أنواع سلسلة حرفية تمثل جميع المسارات الصالحة عبر كائن متداخل:

تنفيذ PathKeys:
type PathKeys<T> = T extends object
  ? {
      [K in keyof T]: K extends string
        ? T[K] extends object
          ? `${K}` | `${K}.${PathKeys<T[K]>}`
          : `${K}`
        : never;
    }[keyof T]
  : never;

// مثال الاستخدام
interface Settings {
  user: {
    name: string;
    email: string;
    preferences: {
      theme: string;
      language: string;
    };
  };
  app: {
    version: string;
    features: {
      darkMode: boolean;
      notifications: boolean;
    };
  };
}

type SettingPath = PathKeys<Settings>;
// نوع النتيجة:
// "user" | "user.name" | "user.email" | "user.preferences" |
// "user.preferences.theme" | "user.preferences.language" |
// "app" | "app.version" | "app.features" |
// "app.features.darkMode" | "app.features.notifications"

// دالة getter آمنة من حيث النوع
function getNestedValue<T, P extends PathKeys<T>>(
  obj: T,
  path: P
): any {
  return path.split('.').reduce((acc: any, key) => acc?.[key], obj);
}

const settings: Settings = {
  user: {
    name: 'أحمد',
    email: 'ahmad@example.com',
    preferences: {
      theme: 'dark',
      language: 'ar',
    },
  },
  app: {
    version: '1.0.0',
    features: {
      darkMode: true,
      notifications: false,
    },
  },
};

const theme = getNestedValue(settings, 'user.preferences.theme'); // صالح
// const invalid = getNestedValue(settings, 'user.invalid.path'); // خطأ TypeScript

Mutable - إزالة معدّلات Readonly

أنشئ نسخة قابلة للكتابة من نوع readonly:

تنفيذ Mutable:
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// نسخة عميقة
type DeepMutable<T> = {
  -readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepMutable<U>>
      : DeepMutable<T[P]>
    : T[P];
};

// مثال
interface ReadonlyUser {
  readonly id: number;
  readonly name: string;
  readonly profile: {
    readonly bio: string;
  };
}

type WritableUser = DeepMutable<ReadonlyUser>;

const user: WritableUser = {
  id: 1,
  name: 'أحمد',
  profile: { bio: 'مطور' },
};

user.name = 'فاطمة'; // مسموح
user.profile.bio = 'مصممة'; // مسموح
تحذير: استخدم Mutable بحذر. أنواع Readonly موجودة لسبب - إزالة معدّلات readonly يمكن أن تعرض ضمانات عدم التغيير التي تعتمد عليها أجزاء أخرى من قاعدة الكود الخاصة بك للخطر.

PickByType - اختيار الخصائص حسب نوع القيمة

أنشئ نوعًا يحتوي فقط على الخصائص التي تطابق قيمها نوعًا محددًا:

تنفيذ PickByType:
type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P];
};

// العكس: OmitByType
type OmitByType<T, U> = {
  [P in keyof T as T[P] extends U ? never : P]: T[P];
};

// مثال الاستخدام
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  inStock: boolean;
  rating: number;
  category: string;
}

// احصل على خصائص السلسلة فقط
type StringProps = PickByType<Product, string>;
// النتيجة: { name: string; description: string; category: string }

// احصل على خصائص الرقم فقط
type NumberProps = PickByType<Product, number>;
// النتيجة: { id: number; price: number; rating: number }

// احصل على الخصائص غير السلسلة فقط
type NonStringProps = OmitByType<Product, string>;
// النتيجة: { id: number; price: number; inStock: boolean; rating: number }

RequireAtLeastOne - طلب واحدة من عدة خصائص

تأكد من توفير خاصية واحدة على الأقل من مجموعة:

تنفيذ RequireAtLeastOne:
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
  T,
  Exclude<keyof T, Keys>
> &
  {
    [K in Keys]-?: Required<Pick<T, K>> &
      Partial<Pick<T, Exclude<Keys, K>>>;
  }[Keys];

// مثال الاستخدام
interface ContactInfo {
  name: string;
  email?: string;
  phone?: string;
  address?: string;
}

type ContactRequired = RequireAtLeastOne<
  ContactInfo,
  'email' | 'phone' | 'address'
>;

// صالح: يحتوي على البريد الإلكتروني
const contact1: ContactRequired = {
  name: 'أحمد',
  email: 'ahmad@example.com',
};

// صالح: يحتوي على الهاتف
const contact2: ContactRequired = {
  name: 'فاطمة',
  phone: '555-0123',
};

// صالح: يحتوي على متعددة
const contact3: ContactRequired = {
  name: 'محمد',
  email: 'mohamed@example.com',
  phone: '555-0456',
};

// خطأ TypeScript: يجب أن يحتوي على واحدة على الأقل من email أو phone أو address
// const invalid: ContactRequired = {
//   name: 'غير صالح',
// };

RequireExactlyOne - طلب خاصية واحدة بالضبط

تأكد من توفير خاصية واحدة بالضبط من مجموعة (خصائص حصرية متبادلة):

تنفيذ RequireExactlyOne:
type RequireExactlyOne<T, Keys extends keyof T = keyof T> = Pick<
  T,
  Exclude<keyof T, Keys>
> &
  {
    [K in Keys]-?: Required<Pick<T, K>> &
      Partial<Record<Exclude<Keys, K>, undefined>>;
  }[Keys];

// مثال الاستخدام
interface SearchParams {
  query?: string;
  userId?: number;
  email?: string;
}

type ExactSearch = RequireExactlyOne<
  SearchParams,
  'query' | 'userId' | 'email'
>;

// صالح: خاصية واحدة بالضبط
const search1: ExactSearch = { query: 'typescript' };
const search2: ExactSearch = { userId: 123 };
const search3: ExactSearch = { email: 'user@example.com' };

// خطأ TypeScript: لا يمكن توفير عدة خصائص
// const invalid1: ExactSearch = { query: 'ts', userId: 1 };

// خطأ TypeScript: يجب توفير خاصية واحدة بالضبط
// const invalid2: ExactSearch = {};

Prettify - تسطيح أنواع التقاطع لعرض أفضل في IDE

أنواع التقاطع في TypeScript يمكن أن يكون من الصعب قراءتها في تلميحات IDE. Prettify يسطحها:

تنفيذ Prettify:
type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

// مثال
type Base = { id: number; name: string };
type Extended = Base & { email: string; age: number };

// بدون Prettify، يعرض IDE: Base & { email: string; age: number }
type Ugly = Extended;

// مع Prettify، يعرض IDE: { id: number; name: string; email: string; age: number }
type Pretty = Prettify<Extended>;
نصيحة: استخدم Prettify عند إنشاء أنواع أدوات مساعدة معقدة لجعل تلميحات IDE أكثر قابلية للقراءة. لا يغير سلوك النوع، فقط كيفية عرضه.

PartialBy - جعل خصائص محددة اختيارية

اجعل خصائص محددة فقط اختيارية مع الحفاظ على الخصائص الأخرى مطلوبة:

تنفيذ PartialBy:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// مثال
interface User {
  id: number;
  email: string;
  username: string;
  firstName: string;
  lastName: string;
}

// اجعل البريد الإلكتروني واسم المستخدم فقط اختياريًا
type UserUpdate = PartialBy<User, 'email' | 'username'>;

const update: UserUpdate = {
  id: 1,
  firstName: 'أحمد',
  lastName: 'علي',
  // email و username اختياريان
};

بناء مكتبة شاملة لأنواع الأدوات المساعدة

اجمع الأدوات المساعدة في مكتبة قابلة لإعادة الاستخدام:

مكتبة أدوات مساعدة كاملة:
// types/utils.ts

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepPartial<U>>
      : DeepPartial<T[P]>
    : T[P];
};

export type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Array<infer U>
      ? ReadonlyArray<DeepReadonly<U>>
      : DeepReadonly<T[P]>
    : T[P];
};

export type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepRequired<U>>
      : DeepRequired<T[P]>
    : T[P];
};

export type DeepMutable<T> = {
  -readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepMutable<U>>
      : DeepMutable<T[P]>
    : T[P];
};

export type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P];
};

export type OmitByType<T, U> = {
  [P in keyof T as T[P] extends U ? never : P]: T[P];
};

export type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

export type PartialBy<T, K extends keyof T> = Omit<T, K> &
  Partial<Pick<T, K>>;

export type RequiredBy<T, K extends keyof T> = Omit<T, K> &
  Required<Pick<T, K>>;

// الاستخدام عبر تطبيقك
import { DeepPartial, DeepReadonly, Prettify } from './types/utils';
تمرين:
  1. نفذ أداة مساعدة DeepNonNullable<T> تزيل null و undefined من جميع الخصائص بشكل متكرر
  2. أنشئ أداة مساعدة ValueOf<T> تستخرج جميع أنواع القيم الممكنة من نوع كائن
  3. ابنِ StrictOmit<T, K> يثير خطأ ترجمة إذا كانت K تحتوي على مفاتيح ليست في T (على عكس Omit المدمج)
  4. نفذ PathValue<T, P> يعيد النوع في مسار سلسلة محدد (مثل "user.profile.name")
  5. أنشئ أداة مساعدة PromisifyMethods<T> تلف جميع أنواع إرجاع الطرق في Promises
  6. ابنِ مكتبة أنواع أدوات مساعدة شاملة مع 10 أدوات مساعدة مخصصة على الأقل واستخدمها في نموذج بيانات من العالم الحقيقي

ملخص

في هذا الدرس، استكشفت أنماط أنواع الأدوات المساعدة المتقدمة التي توسع نظام النوع في TypeScript للتعامل مع سيناريوهات العالم الحقيقي المعقدة. تعلمت بناء DeepPartial و DeepReadonly والأدوات المساعدة المتكررة الأخرى، وإنشاء محددات مسار كائن آمنة من حيث النوع مع PathKeys، واختيار الخصائص حسب النوع، وفرض قيود "واحدة على الأقل" و "واحدة بالضبط" على الخصائص، وتسطيح أنواع التقاطع لقابلية قراءة أفضل. تمكّنك هذه الأنماط من إنشاء أدوات نوع متطورة وقابلة لإعادة الاستخدام تقلل من الكود الزائد، وتفرض قيودًا معقدة، وتجعل قاعدة الكود الخاصة بك أكثر قابلية للصيانة وأمانًا من حيث النوع.