React مع TypeScript
دمج TypeScript مع React
TypeScript يضيف فحص الأنواع الثابتة إلى JavaScript، مما يساعد على اكتشاف الأخطاء في وقت الترجمة وتحسين جودة الكود. في هذا الدرس، سنتعلم كيفية استخدام TypeScript بفعالية مع React لبناء تطبيقات أكثر قابلية للصيانة وقوة.
لماذا TypeScript مع React؟
TypeScript يوفر عدة فوائد لتطوير React:
- أمان الأنواع: اكتشاف الأخطاء أثناء التطوير بدلاً من وقت التشغيل
- IntelliSense أفضل: إكمال تلقائي ووثائق محسّنة
- ثقة إعادة الهيكلة: تغيير الكود بأمان مع فحص الأنواع
- كود موثق ذاتياً: الأنواع تعمل كوثائق مضمنة
- تعاون محسّن: عقود واضحة بين المكونات
إعداد TypeScript مع React
إنشاء تطبيق React جديد مع TypeScript:
# مشروع جديد مع TypeScript
npx create-react-app my-app --template typescript
# إضافة TypeScript لمشروع موجود
npm install --save typescript @types/node @types/react @types/react-dom
npm install --save-dev @types/jest
# إنشاء tsconfig.json
npx tsc --init
# إنشاء مشروع Vite + React + TypeScript جديد
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
كتابة أنواع مكونات الدالة
أنماط أساسية لكتابة أنواع مكونات دالة React:
// نوع الإرجاع الصريح (اختياري لكن موصى به)
function Greeting(): JSX.Element {
return <h1>Hello, World!</h1>;
}
// دالة سهم مع نوع صريح
const Greeting: React.FC = () => {
return <h1>Hello, World!</h1>;
};
// الأكثر إيجازاً (استنتاج النوع)
function Greeting() {
return <h1>Hello, World!</h1>;
}
React.FC أو React.FunctionComponent. استخدم بناء الدالة العادي ودع TypeScript يستنتج نوع الإرجاع أو اكتبه صراحةً كـ JSX.Element.
كتابة أنواع الخصائص
تعريف أنواع الخصائص باستخدام interfaces أو type aliases:
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean; // خاصية اختيارية
variant?: 'primary' | 'secondary'; // نوع اتحاد
}
function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
// الاستخدام
<Button label="Click Me" onClick={() => console.log('Clicked')} />
<Button label="Submit" onClick={handleSubmit} variant="secondary" />
interface CardProps {
title: string;
children: React.ReactNode; // يقبل أي أطفال React صالحين
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// الاستخدام
<Card title="My Card">
<p>This is the card content</p>
<button>Action</button>
</Card>
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
interface UserListProps {
users: User[];
onUserClick: (user: User) => void;
loading?: boolean;
error?: string | null;
renderEmpty?: () => React.ReactNode;
}
function UserList({
users,
onUserClick,
loading = false,
error = null,
renderEmpty
}: UserListProps) {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (users.length === 0) {
return renderEmpty ? <>{renderEmpty()}</> : <p>No users found</p>;
}
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserClick(user)}>
{user.name} ({user.email})
</li>
))}
</ul>
);
}
{} كنوع (مثل children: {}). استخدم React.ReactNode للأطفال، object للكائنات البسيطة، أو عرّف interfaces محددة.
كتابة أنواع الحالة
TypeScript يمكنه استنتاج أنواع الحالة، لكن الكتابة الصريحة تساعد مع الحالة المعقدة:
import { useState } from 'react';
function Counter() {
// النوع مستنتج كـ number
const [count, setCount] = useState(0);
// نوع صريح (مفيد للأنواع المعقدة)
const [count, setCount] = useState<number>(0);
// حالة نصية
const [name, setName] = useState('John');
// حالة منطقية
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
interface FormData {
username: string;
email: string;
age: number;
}
function UserForm() {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
age: 0
});
const handleChange = (field: keyof FormData, value: string | number) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
return (
<form>
<input
value={formData.username}
onChange={e => handleChange('username', e.target.value)}
/>
<input
type="email"
value={formData.email}
onChange={e => handleChange('email', e.target.value)}
/>
<input
type="number"
value={formData.age}
onChange={e => handleChange('age', parseInt(e.target.value))}
/>
</form>
);
}
type Status = 'idle' | 'loading' | 'success' | 'error';
interface DataState<T> {
status: Status;
data: T | null;
error: string | null;
}
function DataFetcher() {
const [state, setState] = useState<DataState<User[]>>({
status: 'idle',
data: null,
error: null
});
const fetchData = async () => {
setState({ status: 'loading', data: null, error: null });
try {
const response = await fetch('/api/users');
const data = await response.json();
setState({ status: 'success', data, error: null });
} catch (error) {
setState({
status: 'error',
data: null,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
};
return (
<div>
{state.status === 'loading' && <p>Loading...</p>}
{state.status === 'error' && <p>Error: {state.error}</p>}
{state.status === 'success' && state.data && (
<ul>
{state.data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
كتابة أنواع الأحداث
أنواع أحداث React في TypeScript:
import { ChangeEvent, FormEvent, MouseEvent, KeyboardEvent } from 'react';
function EventExamples() {
// تغيير الإدخال
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// تغيير منطقة النص
const handleTextareaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
console.log(e.target.value);
};
// تغيير التحديد
const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value);
};
// إرسال النموذج
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Form submitted');
};
// نقر الزر
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked', e.clientX, e.clientY);
};
// أحداث لوحة المفاتيح
const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyPress={handleKeyPress} />
<textarea onChange={handleTextareaChange} />
<select onChange={handleSelectChange}>
<option>Option 1</option>
</select>
<button onClick={handleClick}>Submit</button>
</form>
);
}
EventType<HTMLElementType>. دع IDE الخاص بك يقترح الأنواع الصحيحة بناءً على العنصر.
المكونات العامة
إنشاء مكونات قابلة لإعادة الاستخدام مع الأنواع العامة:
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyMessage?: string;
}
function List<T>({
items,
renderItem,
keyExtractor,
emptyMessage = 'No items found'
}: ListProps<T>) {
if (items.length === 0) {
return <p>{emptyMessage}</p>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// الاستخدام مع أنواع مختلفة
interface User {
id: number;
name: string;
}
interface Product {
id: string;
title: string;
price: number;
}
function App() {
const users: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
const products: Product[] = [
{ id: 'p1', title: 'Laptop', price: 999 },
{ id: 'p2', title: 'Mouse', price: 29 }
];
return (
<>
<List
items={users}
keyExtractor={user => user.id}
renderItem={user => <span>{user.name}</span>}
/>
<List
items={products}
keyExtractor={product => product.id}
renderItem={product => (
<div>
{product.title} - ${product.price}
</div>
)}
/>
</>
);
}
interface Option<T> {
value: T;
label: string;
}
interface SelectProps<T> {
options: Option<T>[];
value: T;
onChange: (value: T) => void;
placeholder?: string;
}
function Select<T extends string | number>({
options,
value,
onChange,
placeholder = 'Select an option'
}: SelectProps<T>) {
return (
<select
value={value as string}
onChange={e => {
const selectedOption = options.find(
opt => String(opt.value) === e.target.value
);
if (selectedOption) {
onChange(selectedOption.value);
}
}}
>
<option value="">{placeholder}</option>
{options.map(option => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</select>
);
}
// الاستخدام
function App() {
const [selectedId, setSelectedId] = useState<number>(0);
const [selectedColor, setSelectedColor] = useState<string>('');
return (
<>
<Select
options={[
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' }
]}
value={selectedId}
onChange={setSelectedId}
/>
<Select
options={[
{ value: 'red', label: 'Red' },
{ value: 'blue', label: 'Blue' }
]}
value={selectedColor}
onChange={setSelectedColor}
/>
</>
);
}
Interfaces مقابل Type Aliases
كلاهما يمكن أن يحدد خصائص المكون، لكن لهما حالات استخدام مختلفة:
// Interfaces يمكن توسيعها ودمجها
interface BaseProps {
id: string;
className?: string;
}
interface ButtonProps extends BaseProps {
label: string;
onClick: () => void;
}
// دمج التصريح (مفيد للمكتبات)
interface Window {
myCustomProperty: string;
}
// Types أفضل للاتحادات والأنواع المعقدة
type Status = 'idle' | 'loading' | 'success' | 'error';
type ButtonVariant = 'primary' | 'secondary' | 'danger';
type ResponseData<T> = {
data: T;
status: number;
} | {
error: string;
status: number;
};
// Types يمكن أن تستخدم أنواع مُعيّنة
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// Types يمكن أن تستخدم أنواع شرطية
type NonNullable<T> = T extends null | undefined ? never : T;
interface لأشكال الكائنات وخصائص المكونات. استخدم type للاتحادات والتقاطعات وتحويلات الأنواع المعقدة.
تمرين 1: تطبيق مهام مع TypeScript
ابنِ تطبيق مهام مع أنواع TypeScript كاملة:
- عرّف interface Todo مع id، text، completed، createdAt
- أنشئ مكون TodoList مع أنواع خصائص مناسبة
- اكتب أنواع جميع معالجات الأحداث بشكل صحيح
- استخدم أنواع حالة مناسبة لقائمة المهام
- نفّذ وظائف الإضافة والتبديل والحذف والتصفية
تمرين 2: جدول بيانات عام
أنشئ مكون جدول بيانات قابل لإعادة الاستخدام وعام:
- اقبل نوع بيانات عام
- عرّف تكوين الأعمدة مع وصول آمن من حيث النوع
- نفّذ الفرز مع أنواع مناسبة
- أضف ترقيم الصفحات مع معالجات مكتوبة
- دعم عارضات خلايا مخصصة
تمرين 3: بناء نموذج
ابنِ مكون نموذج آمن من حيث النوع:
- أنشئ interface FormField مع أنواع حقول مختلفة
- اكتب نوع حالة النموذج وأخطاء التحقق
- نفّذ إرسال النموذج الآمن من حيث النوع
- أضف التحقق على مستوى الحقل ومستوى النموذج
- دعم أنواع إدخال مختلفة مع كتابة مناسبة