لغة TypeScript
أفضل ممارسات TypeScript
أفضل ممارسات TypeScript
كتابة كود TypeScript قابل للصيانة وآمن من حيث النوع يتطلب اتباع أفضل الممارسات المعتمدة. في هذا الدرس، سنغطي تكوين الوضع الصارم، تجنب نوع any، استراتيجيات الكتابة المناسبة، تكوين الفحص، والأخطاء الشائعة التي يجب تجنبها.
تفعيل الوضع الصارم
قم دائماً بتفعيل الوضع الصارم لأقصى أمان للأنواع:
// tsconfig.json
{
"compilerOptions": {
"strict": true, // يفعل جميع الفحوصات الصارمة
// أو قم بالتفعيل بشكل فردي:
"strictNullChecks": true, // null/undefined يجب أن تكون صريحة
"strictFunctionTypes": true, // معاملات الدوال متناقضة
"strictBindCallApply": true, // فحص نوع bind/call/apply
"strictPropertyInitialization": true, // خصائص الفئة يجب تهيئتها
"noImplicitThis": true, // خطأ على 'this' غير واضح
"alwaysStrict": true, // استخدام الوضع الصارم في الإخراج
"noImplicitAny": true, // خطأ على 'any' ضمني
// فحوصات مفيدة إضافية
"noUnusedLocals": true, // خطأ على المتغيرات غير المستخدمة
"noUnusedParameters": true, // خطأ على المعاملات غير المستخدمة
"noImplicitReturns": true, // جميع مسارات الكود يجب أن تُرجع
"noFallthroughCasesInSwitch": true, // حالات Switch يجب أن تنقطع/تُرجع
"noUncheckedIndexedAccess": true // الوصول بالفهرس يُرجع T | undefined
}
}
ملاحظة: تفعيل
strict: true يفعل تلقائياً جميع خيارات فحص الأنواع الصارمة. إنها الخط الأساسي الموصى به لجميع مشاريع TypeScript.
تجنب نوع 'any'
نوع any يهزم الغرض من TypeScript. استخدم البدائل:
// ❌ سيء: استخدام any
function processData(data: any) {
return data.value.toUpperCase(); // لا يوجد فحص نوع!
}
// ✅ جيد: استخدم unknown للأنواع المجهولة حقاً
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
const obj = data as { value: unknown };
if (typeof obj.value === 'string') {
return obj.value.toUpperCase();
}
}
throw new Error('Invalid data');
}
// ✅ أفضل: عرف أنواعاً مناسبة
interface Data {
value: string;
}
function processData(data: Data) {
return data.value.toUpperCase();
}
// ✅ جيد: استخدم الأنواع العامة للأنواع المرنة
function processData<T extends { value: string }>(data: T) {
return data.value.toUpperCase();
}
متى تستخدم 'unknown' مقابل 'any'
// unknown: طريقة آمنة من حيث النوع لتمثيل القيم المجهولة
function parseJSON(json: string): unknown {
return JSON.parse(json);
}
// يجب تضييق النوع قبل الاستخدام
const result = parseJSON('{"name": "John"}');
// حارس النوع مطلوب
if (typeof result === 'object' && result !== null && 'name' in result) {
console.log((result as { name: string }).name);
}
// any: استخدم فقط عند الضرورة القصوى (مثل ترحيل JS)
// يفضل unknown بدلاً من ذلك
declare function legacyAPI(): any; // كود قديم
تحذير: استخدم
any فقط كملاذ أخير. يعطل فحص النوع ويمكن أن يخفي الأخطاء. يفضل unknown، والذي يتطلب تضييق النوع قبل الاستخدام.
معالجة Null وUndefined بشكل صحيح
// ❌ سيء: عدم معالجة null/undefined
interface User {
name: string;
email?: string; // اختياري
}
function sendEmail(user: User) {
const email = user.email;
// خطأ مع strictNullChecks: email قد تكون undefined
return email.toLowerCase();
}
// ✅ جيد: فحوصات صريحة
function sendEmail(user: User) {
if (!user.email) {
throw new Error('Email required');
}
return user.email.toLowerCase(); // آمن الآن
}
// ✅ جيد: السلسلة الاختيارية
function sendEmail(user: User) {
return user.email?.toLowerCase() ?? 'no-email';
}
// ✅ جيد: الدمج الخالي
function getDisplayName(name: string | null | undefined) {
return name ?? 'Anonymous';
}
تأكيدات الأنواع مقابل حراس الأنواع
// ❌ سيء: تأكيدات الأنواع بدون تحقق
function processResponse(response: unknown) {
const data = response as { items: string[] };
return data.items.length; // غير آمن!
}
// ✅ جيد: حراس الأنواع مع التحقق في وقت التشغيل
interface ApiResponse {
items: string[];
}
function isApiResponse(value: unknown): value is ApiResponse {
return (
typeof value === 'object' &&
value !== null &&
'items' in value &&
Array.isArray((value as ApiResponse).items) &&
(value as ApiResponse).items.every(item => typeof item === 'string')
);
}
function processResponse(response: unknown) {
if (!isApiResponse(response)) {
throw new Error('Invalid response');
}
return response.items.length; // آمن!
}
// ✅ جيد: استخدم مكتبات مثل zod للتحقق
import { z } from 'zod';
const ApiResponseSchema = z.object({
items: z.array(z.string())
});
type ApiResponse = z.infer<typeof ApiResponseSchema>;
function processResponse(response: unknown) {
const data = ApiResponseSchema.parse(response);
return data.items.length; // تم التحقق والكتابة!
}
يفضل الواجهات على أسماء الأنواع (عادة)
// ✅ جيد: الواجهات لأشكال الكائنات
interface User {
id: string;
name: string;
email: string;
}
// يمكن توسيع الواجهات ودمجها
interface User {
createdAt: Date; // دمج التصريح
}
interface Admin extends User {
permissions: string[];
}
// ✅ جيد: أسماء الأنواع للاتحادات، التقاطعات، الأنواع الأولية
type Status = 'pending' | 'approved' | 'rejected';
type ID = string | number;
type Nullable<T> = T | null;
// ✅ جيد: أسماء الأنواع للأنواع المعقدة
type ApiResponse<T> = {
data: T;
status: number;
} | {
error: string;
status: number;
};
نصيحة: استخدم الواجهات لأنواع الكائنات التي قد يتم توسيعها. استخدم أسماء الأنواع للاتحادات، التقاطعات، والأنواع المعينة.
كتابة الدوال بشكل صحيح
// ❌ سيء: أنواع إرجاع ضمنية
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// ✅ جيد: أنواع صريحة
function calculateTotal(items: Array<{ price: number }>): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// ✅ جيد: استخدم readonly للثبات
function calculateTotal(items: ReadonlyArray<{ readonly price: number }>): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// ✅ جيد: الدوال غير المتزامنة يجب أن تُرجع Promise
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// ❌ سيء: استدعاء رد بدون أنواع مناسبة
function fetchData(callback) {
callback(null, data);
}
// ✅ جيد: استدعاء رد مكتوب بشكل صحيح
function fetchData(callback: (error: Error | null, data: string) => void): void {
callback(null, 'data');
}
كتابة المصفوفات والكائنات
// ✅ يفضل صيغة Array<T> للأنواع المعقدة
const users: Array<User> = [];
const matrix: Array<Array<number>> = [];
// ✅ T[] جيد للأنواع البسيطة
const numbers: number[] = [1, 2, 3];
const strings: string[] = ['a', 'b', 'c'];
// ✅ استخدم readonly للمصفوفات الثابتة
function processItems(items: readonly string[]): void {
// items.push('new'); // Error: readonly
console.log(items.length); // OK
}
// ✅ أنواع الصفوف للمصفوفات ذات الطول الثابت
type Point = [number, number];
type RGB = [red: number, green: number, blue: number];
const point: Point = [10, 20];
const color: RGB = [255, 128, 0];
// ✅ استخدم Record للكائنات ذات المفاتيح الديناميكية
const userMap: Record<string, User> = {
'user1': { id: '1', name: 'John', email: 'john@example.com' },
'user2': { id: '2', name: 'Jane', email: 'jane@example.com' }
};
// ✅ توقيعات الفهرس للكائنات المرنة
interface Dictionary {
[key: string]: number;
}
const scores: Dictionary = {
math: 95,
science: 87
};
أفضل ممارسات الفئات
// ✅ استخدم معدلات الوصول
class User {
private id: string;
protected name: string;
public email: string;
// ✅ خصائص للقراءة فقط
readonly createdAt: Date;
// ✅ خصائص المعاملات (اختصار)
constructor(
id: string,
private password: string,
public readonly username: string
) {
this.id = id;
this.name = username;
this.email = '';
this.createdAt = new Date();
}
// ✅ أنواع إرجاع صريحة
getId(): string {
return this.id;
}
// ✅ استخدم protected للوراثة
protected setName(name: string): void {
this.name = name;
}
}
// ✅ فئات مجردة للفئات الأساسية
abstract class BaseRepository<T> {
protected items: T[] = [];
abstract validate(item: T): boolean;
save(item: T): void {
if (!this.validate(item)) {
throw new Error('Invalid item');
}
this.items.push(item);
}
}
class UserRepository extends BaseRepository<User> {
validate(user: User): boolean {
return user.email.includes('@');
}
}
تكوين ESLint
// .eslintrc.json
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
// فرض أنواع إرجاع صريحة
"@typescript-eslint/explicit-function-return-type": "warn",
// منع استخدام any
"@typescript-eslint/no-explicit-any": "error",
// منع المتغيرات غير المستخدمة
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
// طلب واردات نوع متسقة
"@typescript-eslint/consistent-type-imports": "error",
// فرض اصطلاحات التسمية
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"]
},
{
"selector": "typeAlias",
"format": ["PascalCase"]
},
{
"selector": "variable",
"modifiers": ["const"],
"format": ["camelCase", "UPPER_CASE"]
}
],
// منع الوعود العائمة
"@typescript-eslint/no-floating-promises": "error",
// طلب اتساق صيغة نوع المصفوفة
"@typescript-eslint/array-type": ["error", {
"default": "array-simple"
}]
}
}
الأخطاء الشائعة التي يجب تجنبها
// ❌ خطأ 1: السلسلة الاختيارية مع آثار جانبية
const user = getUser();
user?.updateLastLogin(); // يُرجع undefined إذا كان user null، لا يوجد خطأ!
// ✅ إصلاح: فحص صريح عندما يهم التأثير الجانبي
if (user) {
user.updateLastLogin();
}
// ❌ خطأ 2: استخدام ! (تأكيد غير null) بشكل غير آمن
function processUser(userId: string) {
const user = users.find(u => u.id === userId)!;
return user.name; // ينهار إذا لم يُعثر على المستخدم!
}
// ✅ إصلاح: معالجة حالة null
function processUser(userId: string): string {
const user = users.find(u => u.id === userId);
if (!user) {
throw new Error('User not found');
}
return user.name;
}
// ❌ خطأ 3: توسيع الأنواع بلا داع
let status = 'pending'; // النوع: string (واسع جداً)
status = 'invalid'; // مسموح ولكن خطأ!
// ✅ إصلاح: استخدم const أو نوع صريح
const status = 'pending'; // النوع: 'pending' (حرفي)
let status: 'pending' | 'approved' | 'rejected' = 'pending';
// ❌ خطأ 4: تغيير المصفوفات readonly
function addItem(items: readonly string[], item: string) {
items.push(item); // Error: readonly
}
// ✅ إصلاح: إرجاع مصفوفة جديدة
function addItem(items: readonly string[], item: string): string[] {
return [...items, item];
}
// ❌ خطأ 5: حراس الأنواع التي لا تضيق بشكل صحيح
function isString(value: unknown) {
return typeof value === 'string'; // يُرجع boolean، لا يضيق النوع
}
// ✅ إصلاح: استخدم محمول نوع مناسب
function isString(value: unknown): value is string {
return typeof value === 'string';
}
أفضل ممارسات الاستيراد/التصدير
// ✅ استخدم واردات النوع للأنواع
import type { User, Admin } from './types';
import { createUser } from './user';
// ✅ افصل تصدير النوع والقيمة
export type { User, Admin };
export { createUser, updateUser };
// ✅ استخدم صادرات البرميل بحذر (يمكن أن تؤثر على حجم الحزمة)
// index.ts
export * from './user';
export * from './admin';
export type * from './types'; // صدّر الأنواع فقط
// ❌ تجنب التبعيات الدائرية
// file1.ts
import { funcB } from './file2';
export function funcA() { funcB(); }
// file2.ts
import { funcA } from './file1'; // دائري!
export function funcB() { funcA(); }
تمرين: أعد هيكلة الكود لاتباع أفضل الممارسات:
- فعّل الوضع الصارم في مشروع TypeScript موجود
- استبدل جميع أنواع
anyبأنواع مناسبة أوunknown - أضف أنواع إرجاع صريحة لجميع الدوال
- نفذ معالجة null/undefined المناسبة مع السلسلة الاختيارية
- أضف حراس الأنواع لجميع عمليات التحقق من البيانات الخارجية
- قم بتكوين ESLint مع قواعد TypeScript
- أصلح جميع أخطاء وتحذيرات الفحص
الخلاصة
في هذا الدرس، تعلمت أفضل ممارسات TypeScript لكتابة كود قابل للصيانة وآمن من حيث النوع. غطينا الوضع الصارم، تجنب any، معالجة null المناسبة، حراس الأنواع، كتابة الدوال، أنماط الفئات، تكوين ESLint، والأخطاء الشائعة. اتباع هذه الممارسات سيساعدك على كتابة تطبيقات TypeScript قوية.