لغة TypeScript

بناء مكتبات آمنة من حيث النوع

25 دقيقة الدرس 37 من 40

بناء مكتبات آمنة من حيث النوع

إنشاء مكتبات TypeScript يتطلب اهتماماً دقيقاً بأمان الأنواع، تصميم واجهة برمجة التطبيقات، وتجربة المطور. في هذا الدرس، سنستكشف أنماط المكتبات العامة، تحميلات الدوال الزائدة، تحسين استنتاج الأنواع، وكيفية نشر المكتبات مع تعريفات أنواع مناسبة.

هيكل المكتبة

مكتبة TypeScript منظمة بشكل جيد يجب أن تحتوي على:

my-library/ ├── src/ │ ├── index.ts # نقطة الدخول الرئيسية │ ├── types.ts # تعريفات الأنواع │ ├── core/ # الوظائف الأساسية │ └── utils/ # دوال الأدوات المساعدة ├── dist/ # الإخراج المترجم │ ├── index.js # CommonJS │ ├── index.d.ts # تصريحات الأنواع │ └── index.esm.js # ES modules ├── package.json ├── tsconfig.json └── README.md

tsconfig.json للمكتبات

{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "declaration": true, // توليد ملفات .d.ts "declarationMap": true, // توليد .d.ts.map لتصحيح الأخطاء "outDir": "./dist", "rootDir": "./src", "removeComments": false, // الاحتفاظ بتعليقات JSDoc "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] }

أنماط المكتبات العامة

الأنواع العامة هي أساس المكتبات القابلة لإعادة الاستخدام والآمنة من حيث النوع:

// أدوات مساعدة عامة للمجموعات export class Collection<T> { private items: T[] = []; add(item: T): this { this.items.push(item); return this; // تسلسل الطرق } remove(predicate: (item: T) => boolean): T | undefined { const index = this.items.findIndex(predicate); if (index === -1) return undefined; return this.items.splice(index, 1)[0]; } find(predicate: (item: T) => boolean): T | undefined { return this.items.find(predicate); } filter(predicate: (item: T) => boolean): Collection<T> { const filtered = new Collection<T>(); filtered.items = this.items.filter(predicate); return filtered; } map<U>(mapper: (item: T) => U): Collection<U> { const mapped = new Collection<U>(); mapped.items = this.items.map(mapper); return mapped; } toArray(): T[] { return [...this.items]; } get length(): number { return this.items.length; } } // الاستخدام const numbers = new Collection<number>(); numbers.add(1).add(2).add(3); const doubled = numbers.map(n => n * 2); // Collection<number> const strings = numbers.map(n => n.toString()); // Collection<string>

قيود عامة متقدمة

// نوع عام مع قيود interface Identifiable { id: string | number; } export class Repository<T extends Identifiable> { private items = new Map<string | number, T>(); save(item: T): void { this.items.set(item.id, item); } findById(id: string | number): T | undefined { return this.items.get(id); } update(id: string | number, updater: (item: T) => Partial<T>): T | undefined { const item = this.items.get(id); if (!item) return undefined; const updates = updater(item); const updated = { ...item, ...updates }; this.items.set(id, updated); return updated; } delete(id: string | number): boolean { return this.items.delete(id); } findAll(): T[] { return Array.from(this.items.values()); } } // الاستخدام مع القيد interface User extends Identifiable { id: string; name: string; email: string; } const userRepo = new Repository<User>(); userRepo.save({ id: '1', name: 'John', email: 'john@example.com' }); // خطأ نوع: خاصية 'id' مفقودة // const invalidRepo = new Repository<{ name: string }>(); // Error!

تحميلات الدوال الزائدة

تحميلات الدوال الزائدة توفر توقيعات أنواع متعددة لحالات استخدام مختلفة:

// منشئ الاستعلام مع التحميلات الزائدة export interface QueryOptions { limit?: number; offset?: number; sort?: string; } export class QueryBuilder<T> { // التحميل الزائد 1: استعلام مع كائن خيارات query(options: QueryOptions): Promise<T[]>; // التحميل الزائد 2: استعلام مع حد فقط query(limit: number): Promise<T[]>; // التحميل الزائد 3: استعلام مع حد وإزاحة query(limit: number, offset: number): Promise<T[]>; // توقيع التنفيذ (غير مرئي للمستهلكين) query( limitOrOptions: number | QueryOptions, offset?: number ): Promise<T[]> { let options: QueryOptions; if (typeof limitOrOptions === 'number') { options = { limit: limitOrOptions, offset }; } else { options = limitOrOptions; } return this.executeQuery(options); } private async executeQuery(options: QueryOptions): Promise<T[]> { // التنفيذ return []; } } // الاستخدام - جميعها آمنة من حيث النوع const builder = new QueryBuilder<User>(); await builder.query({ limit: 10, sort: 'name' }); // التحميل الزائد 1 await builder.query(10); // التحميل الزائد 2 await builder.query(10, 20); // التحميل الزائد 3

تحسين استنتاج الأنواع

// نوع مساعد لاستخراج نوع عنصر المصفوفة type ArrayElement<T> = T extends (infer U)[] ? U : T; // استنتاج نوع الإرجاع من استدعاء الوظيفة export function createProcessor<T, R>( processor: (item: T) => R ) { return { process: (items: T[]): R[] => { return items.map(processor); }, processOne: (item: T): R => { return processor(item); } }; } // الاستخدام مع الاستنتاج التلقائي const numberProcessor = createProcessor((n: number) => n * 2); // النوع المستنتج: { process: (items: number[]) => number[]; processOne: (item: number) => number } const result = numberProcessor.process([1, 2, 3]); // number[] // معالج النصوص - أنواع مختلفة مستنتجة const stringProcessor = createProcessor((s: string) => s.length); const lengths = stringProcessor.process(['a', 'bb', 'ccc']); // number[]

نمط البناء مع واجهة برمجة تطبيقات سلسة

// نمط البناء الآمن من حيث النوع export class HttpRequestBuilder { private url?: string; private method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET'; private headers: Record<string, string> = {}; private body?: unknown; private queryParams: Record<string, string> = {}; setUrl(url: string): this { this.url = url; return this; } setMethod(method: 'GET' | 'POST' | 'PUT' | 'DELETE'): this { this.method = method; return this; } addHeader(key: string, value: string): this { this.headers[key] = value; return this; } setBody<T>(body: T): this { this.body = body; return this; } addQueryParam(key: string, value: string): this { this.queryParams[key] = value; return this; } async execute<T = unknown>(): Promise<T> { if (!this.url) { throw new Error('URL is required'); } const queryString = new URLSearchParams(this.queryParams).toString(); const fullUrl = queryString ? `${this.url}?${queryString}` : this.url; const response = await fetch(fullUrl, { method: this.method, headers: this.headers, body: this.body ? JSON.stringify(this.body) : undefined }); return response.json(); } } // الاستخدام const request = new HttpRequestBuilder() .setUrl('https://api.example.com/users') .setMethod('POST') .addHeader('Content-Type', 'application/json') .addHeader('Authorization', 'Bearer token') .setBody({ name: 'John', email: 'john@example.com' }) .addQueryParam('source', 'web'); const result = await request.execute<User>();
ملاحظة: نمط البناء مع تسلسل الطرق (إرجاع this) يوفر تجربة مطور ممتازة مع الحفاظ على أمان الأنواع.

نمط باعث الأحداث

// باعث أحداث آمن من حيث النوع type EventMap = Record<string, unknown>; export class TypedEventEmitter<Events extends EventMap> { private listeners = new Map<keyof Events, Set<(data: unknown) => void>>(); on<K extends keyof Events>( event: K, listener: (data: Events[K]) => void ): () => void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)!.add(listener as (data: unknown) => void); // إرجاع دالة إلغاء الاشتراك return () => this.off(event, listener); } off<K extends keyof Events>( event: K, listener: (data: Events[K]) => void ): void { const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.delete(listener as (data: unknown) => void); } } emit<K extends keyof Events>(event: K, data: Events[K]): void { const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.forEach(listener => listener(data)); } } once<K extends keyof Events>( event: K, listener: (data: Events[K]) => void ): void { const onceWrapper = (data: Events[K]) => { listener(data); this.off(event, onceWrapper); }; this.on(event, onceWrapper); } } // الاستخدام مع أحداث مكتوبة interface AppEvents { userLogin: { userId: string; timestamp: number }; userLogout: { userId: string }; dataUpdate: { type: string; data: unknown }; } const emitter = new TypedEventEmitter<AppEvents>(); // مستمعون آمنون من حيث النوع emitter.on('userLogin', (data) => { // data مكتوبة كـ { userId: string; timestamp: number } console.log(`User ${data.userId} logged in at ${data.timestamp}`); }); // إصدار مع فحص النوع emitter.emit('userLogin', { userId: '123', timestamp: Date.now() }); // خطأ نوع: شكل بيانات خاطئ // emitter.emit('userLogin', { userId: 123 }); // Error!

نشر تعريفات الأنواع

قم بتكوين package.json لتوزيع الأنواع بشكل صحيح:

{ "name": "my-library", "version": "1.0.0", "main": "dist/index.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.esm.js", "require": "./dist/index.js", "types": "./dist/index.d.ts" }, "./utils": { "import": "./dist/utils/index.esm.js", "require": "./dist/utils/index.js", "types": "./dist/utils/index.d.ts" } }, "files": [ "dist" ], "scripts": { "build": "tsc && tsc --module es2015 --outDir dist/esm", "prepublishOnly": "npm run build" }, "devDependencies": { "typescript": "^5.0.0" } }

JSDoc لأنواع محسنة

/** * يتحقق من صحة عنوان بريد إلكتروني * @param email - عنوان البريد الإلكتروني للتحقق منه * @returns صحيح إذا كان صالحاً، خاطئ بخلاف ذلك * @example * ```typescript * validateEmail('test@example.com'); // true * validateEmail('invalid'); // false * ``` */ export function validateEmail(email: string): boolean { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); } /** * دالة إعادة محاولة عامة مع تأخير تصاعدي * @typeParam T - نوع الإرجاع للعملية * @param operation - العملية غير المتزامنة لإعادة المحاولة * @param options - خيارات تكوين إعادة المحاولة * @returns وعد يحل لنتيجة العملية * @throws {Error} إذا فشلت جميع محاولات إعادة المحاولة */ export async function retry<T>( operation: () => Promise<T>, options: { maxAttempts?: number; initialDelay?: number; maxDelay?: number; } = {} ): Promise<T> { const { maxAttempts = 3, initialDelay = 1000, maxDelay = 10000 } = options; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { if (attempt === maxAttempts) throw error; const delay = Math.min(initialDelay * Math.pow(2, attempt - 1), maxDelay); await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error('Unreachable'); }
نصيحة: تعليقات JSDoc محفوظة في ملفات .d.ts المولدة، مما يوفر توثيق IntelliSense غني لمستهلكي المكتبة.

اختبار أنواع المكتبة

// استخدم تأكيدات الأنواع لاختبار الأنواع import { expectType } from 'tsd'; import { Collection, Repository } from './index'; // اختبار أنواع Collection const collection = new Collection<number>(); expectType<Collection<number>>(collection); const mapped = collection.map(n => n.toString()); expectType<Collection<string>>(mapped); // اختبار قيود Repository interface User { id: string; name: string; } const repo = new Repository<User>(); expectType<Repository<User>>(repo); // يجب أن يسبب هذا خطأ نوع (User يمتد Identifiable) const user = repo.findById('123'); expectType<User | undefined>(user);
تمرين: ابن مكتبة آمنة من حيث النوع:
  1. أنشئ مكتبة إدارة حالة عامة مع إجراءات آمنة من حيث النوع
  2. نفذ تحميلات دوال زائدة لواجهة برمجة تطبيقات مرنة
  3. أضف نمط بناء سلس للتكوين
  4. أنشئ باعث أحداث مكتوب لتغييرات الحالة
  5. اكتب توثيق JSDoc شامل
  6. قم بتكوين package.json exports مناسب للأنواع
  7. اختبر الأنواع باستخدام tsd أو أدوات مماثلة

الخلاصة

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