React: أفضل الممارسات في TypeScript و React
آخر تحديث: 2026-08-26
أثناء قيام فريق «توم» بتطوير وحدة الدفع، حدث خطأ فادح في بيئة الإنتاج: نظرًا لأن مكونًا معينًا كان يتوقع
userIdلكنه تلقىstringمن المصدر، فقد فشل طلب واجهة برمجة التطبيقات (API). ولو كان المشروع يستخدم لغة TypeScript، لكان قد تم الكشف عن عدم تطابق الأنواع هذا خلال مرحلة الترجمة. قرر «توم» اعتماد لغة TypeScript في المشروع بأكمله للكشف عن مثل هذه المشكلات في مرحلة مبكرة.
1. ما ستتعلمه
- تعريفات أنواع الدعائم (استراتيجيات الاختيار بين
interfaceوtype) - كيفية عمل المكون العام
<T> - نظام أنواع الأحداث في React (ChangeEvent / MouseEvent / KeyboardEvent)
- تعليقات النوع الكاملة للخطافات المخصصة
- نصائح لتوسيع نطاق أنواع سمات عناصر HTML
2. المخططات المفاهيمية
يوضح الرسم البياني التالي عملية التحقق من الأنواع التي يقوم بها TypeScript في تدفق البيانات لمكون React:
flowchart LR
subgraph Compilation Phase
A[Parent Component] -->|"Props Type Checking"| B[Child component]
B -->|"State Type Inference"| C[useState]
C -->|"Event Type Validation"| D["onChange / onClick"]
end
subgraph Runtime
E["Actual DOM Event"] --> F["Type Matching"]
F -->|"Through"| G[Execute as usual]
F -->|"Type mismatch"| H[Compilation error]
end
I["interface / type Definition"] --> A
J["Generic Parameters <T>"] --> B
K["React.ChangeEvent"] --> D
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#e8f5e9,stroke:#2e7d32
style D fill:#fff3e0,stroke:#e65100
3. سيناريو واقعي
| سيناريو TypeScript | حل المشكلة | قواعد اللغة الأساسية |
|---|---|---|
| تعريف نوع المعامل | تم تمرير نوع غير صحيح عند الاستدعاء | interface Props { name: string } |
| نوع الحدث | استنتاج نوع المعلمة onChange | e: React.ChangeEvent<HTMLInputElement> |
| المكونات العامة | تحديد معلمات أنواع البيانات للقوائم/الجداول | <T> المعلمات العامة |
| أنواع الخطافات | استدلال الأنواع لـ useState/useRef | useState<string[]> |
| نوع استجابة واجهة برمجة التطبيقات (API) | القيود المفروضة على بنية قيمة الإرجاع للواجهة | interface ApiResponse { data: User[] } |
يتضمن مشروع الدفع الخاص بـ«توم» تدفق البيانات التالي:
Order List Page → PaymentCard Components → AmountInput → Submit API
بدون TypeScript، كان AmountInput يتوقع onSubmit(value: number)، لكن صفحة القائمة أرسلت onSubmit(value: string)، مما أدى إلى تلقي واجهة برمجة التطبيقات "99.99" بدلاً من 99.99، مما تسبب في حدوث خطأ في التسلسل في الخلفية.
تتعامل TypeScript مع هذا الأمر بطريقة بسيطة للغاية — من خلال الإعلان الصريح عن أنواع المعلمات في واجهات المكونات، وأي تباين في الأنواع سيؤدي إلى ظهور خطأ خلال مرحلة npm run build.
(1) تعريفات أنواع خصائص المكونات
هناك طريقتان رئيسيتان لتعريف المعلمات في TypeScript: interface وtype. وفيما يلي القواعد التي تحكم الاختيار بينهما:
| الطريقة | السيناريوهات القابلة للتطبيق | الميزات |
|---|---|---|
interface |
تعريف أنواع الكائنات: الخصائص / الحالة | يدعم دمج الإعلانات، مما يوفر أداءً جيدًا |
type |
أنواع الاتحاد، وأنواع الأدوات، والمجموعات | أكثر مرونة، وتدعم الأنواع المتقاطعة والأنواع الشرطية |
قاعدة عامة: استخدم
interfaceلتعريف المتغيرات المساعدة/الحالة، واستخدمtypeلتعريف أنواع الاتحاد/أنواع المساعدة.
التعريفات الأساسية للمصطلحات
interface ButtonProps {
/** Button Text */
label: string
/** Variant Styles */
variant?: 'primary' | 'danger' | 'default'
/** Button Size */
size?: 'small' | 'medium' | 'large'
/** Is it disabled? */
disabled?: boolean
/** Click to Callback */
onClick: () => void
/** Child elements(Icons on buttons, etc.) */
children?: React.ReactNode
}
function Button({
label,
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
children,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
style={{
padding: size === 'small' ? '4px 12px' : size === 'large' ? '12px 28px' : '8px 20px',
background: variant === 'danger' ? '#ff4d4f' : variant === 'primary' ? '#1890ff' : '#f0f0f0',
color: variant === 'default' ? '#333' : '#fff',
border: 'none',
borderRadius: 6,
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
transition: 'all 0.2s',
}}
>
{children}
{label}
</button>
)
}
▶ المثال 1: أنواع الخصائص المتقدمة — توسيع نطاق سمات HTML الأصلية
في عملية التطوير الفعلية، غالبًا ما يتعين على المكونات تمرير سمات HTML الأصلية (مثل id وclassName وaria-*). يمكنك استخدام ComponentPropsWithoutRef لورثها:
import { ComponentPropsWithoutRef } from 'react'
// Method 1:Extend Native button Properties(Recommendations)
interface PrimaryButtonProps
extends ComponentPropsWithoutRef<'button'> {
/** Loading Status */
loading?: boolean
/** Icon Name */
icon?: string
}
function PrimaryButton({
loading,
icon,
children,
disabled,
...rest // Remaining Native button Properties
}: PrimaryButtonProps) {
return (
<button
{...rest}
disabled={disabled || loading}
style={{
padding: '8px 24px',
background: loading ? '#91d5ff' : '#1890ff',
color: '#fff',
border: 'none',
borderRadius: 6,
cursor: loading ? 'wait' : 'pointer',
}}
>
{loading ? 'Loading......' : icon ? `${icon} ${children}` : children}
</button>
)
}
// Usage — Both native properties and custom properties can be passed in
<PrimaryButton
id="submit-btn"
loading={isSubmitting}
icon=">"
onClick={() => submit()}
aria-label="Submit Form"
>
Submit
</PrimaryButton>
نقطة أساسية: يتضمن ComponentPropsWithoutRef<'button'> تلقائيًا جميع سمات الأزرار الأصلية، مثل onClick وdisabled وid وclassName وstyle وaria-*. استخدم ...rest لتطبيق هذه السمات على عنصر الزر؛ ولا داعي لإعلانها بشكل فردي.
(2) المكونات العامة
تسمح المكونات العامة لأي مكون بالتعامل مع أنواع بيانات متعددة مع الحفاظ على أمان الأنواع. وأكثر حالات الاستخدام شيوعًا هي مكونات القوائم والجداول والمحددات.
▶ المثال 2: مكون قائمة عامة
import { ReactNode } from 'react'
// Generic Interfaces — T For list item types
interface ListProps<T> {
/** Data Sources */
items: T[]
/** Render each item */
renderItem: (item: T, index: number) => ReactNode
/** The Only One key Extract Function */
keyExtractor: (item: T) => string | number
/** Placeholder text when the list is empty */
emptyText?: string
}
// Generic Components — <T,> Grammar(TS for JSX Compatible syntax)
function List<T>({
items,
renderItem,
keyExtractor,
emptyText = 'No data available',
}: ListProps<T>) {
if (items.length === 0) {
return (
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}>
{emptyText}
</div>
)
}
return (
<div>
{items.map((item, index) => (
<div key={keyExtractor(item)} style={{ marginBottom: 8 }}>
{renderItem(item, index)}
</div>
))}
</div>
)
}
// --- Examples of Use ---
interface User {
id: number
name: string
role: 'admin' | 'user'
}
const users: User[] = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'user' },
]
// Automatic Type Inference:List<User>
// items Automatically inferred as User[],renderItem 's item Automatically set to User
function UserList() {
return (
<List
items={users}
keyExtractor={user => user.id}
renderItem={(user, index) => (
<div
style={{
padding: '8px 16px',
background: index % 2 === 0 ? '#fafafa' : '#fff',
borderRadius: 4,
}}
>
<span style={{ fontWeight: 600 }}>{user.name}</span>
<span
style={{
marginLeft: 8,
color: user.role === 'admin' ? '#1890ff' : '#999',
fontSize: 12,
}}
>
{user.role}
</span>
</div>
)}
/>
)
}
الآليات الرئيسية للأدوية الجنيسة:
TفيListProps<T>هو «معلمة نوع» يتم استنتاجها تلقائيًا بواسطة TypeScript عند استخدامها.- عند تمرير
items={users}(من النوعUser[])، يصبحitemفيrenderItemتلقائيًا من النوعUser - يتغير
keyExtractorالخاص بـitemتلقائيًا إلىUserأيضًا، كما أن استدعاءuser.idيوفر تلميحات كاملة عن النوع.
(3) أنواع أحداث React
تقوم React بتغليف أحداث DOM الأصلية باستخدام نظام الأحداث المركب الخاص بها. بالنسبة لكل نوع من أنواع الأحداث، يجب عليك تحديد نوع عنصر HTML المرتبط به من أجل الحصول على النوع الصحيح currentTarget.
| نوع الحدث | العنصر المقابل | السيناريوهات الشائعة |
|---|---|---|
ChangeEvent<HTMLInputElement> |
حقل إدخال / منطقة نصية / قائمة اختيار | حقل نموذج |
ChangeEvent<HTMLSelectElement> |
تحديد | قائمة منسدلة |
MouseEvent<HTMLButtonElement> |
زر / div | النقر |
FormEvent<HTMLFormElement> |
نموذج | إرسال النموذج |
KeyboardEvent<HTMLInputElement> |
الإدخال | اختصار لوحة المفاتيح |
FocusEvent<HTMLInputElement> |
الإدخال | التركيز/عدم التركيز |
▶ المثال 3: استكمال نموذج البحث عن نوع الحدث
import { useState } from 'react'
interface SearchFormProps {
/** Search Callback */
onSearch: (query: string) => Promise<void>
/** Placeholder text */
placeholder?: string
}
function SearchForm({ onSearch, placeholder = 'Search...' }: SearchFormProps) {
const [query, setQuery] = useState('')
const [isSearching, setIsSearching] = useState(false)
// ChangeEvent<HTMLInputElement> — input Value Changes
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
}
// KeyboardEvent<HTMLInputElement> — Keyboard Events
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Escape') {
e.currentTarget.blur() // currentTarget is HTMLInputElement
setQuery('')
}
}
// FormEvent<HTMLFormElement> — Form Submission
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!query.trim()) return
setIsSearching(true)
try {
await onSearch(query.trim())
} finally {
setIsSearching(false)
}
}
// MouseEvent<HTMLButtonElement> — Click the Clear button
function handleClear(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation() // Prevent Event Bubbling
setQuery('')
}
return (
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8 }}>
<div style={{ position: 'relative', flex: 1 }}>
<input
type="text"
value={query}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: 6,
fontSize: 14,
outline: 'none',
boxSizing: 'border-box',
}}
/>
{query && (
<button
type="button"
onClick={handleClear}
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
border: 'none',
background: 'none',
cursor: 'pointer',
color: '#999',
}}
>
x
</button>
)}
</div>
<button
type="submit"
disabled={isSearching || !query.trim()}
style={{
padding: '8px 20px',
background: isSearching ? '#91d5ff' : '#1890ff',
color: '#fff',
border: 'none',
borderRadius: 6,
cursor: isSearching ? 'wait' : 'pointer',
fontSize: 14,
}}
>
{isSearching ? 'Searching......' : 'Search'}
</button>
</form>
)
}
export default SearchForm
النقاط الرئيسية بشأن أنواع الفعاليات:
React.ChangeEvent<HTMLInputElement>— المعلمة العامة هي نوع العنصر المرتبط بالحدث، وهي التي تحدد أنواعe.targetوe.currentTargete.currentTargetهو العنصر المرتبط بالحدث (آمن من حيث النوع)، وe.targetهو العنصر الذي يُطلق الحدث فعليًّا (والذي قد يكون عنصرًا تابعًا)- تُرجع الدالة
KeyboardEventإلىe.keyسلسلة نصية؛ ولا يلزم تحديد نوع إضافي
▶ المثال 4: تعليقات الأنواع للـ«هوكات» المخصصة
تتطلب الخطافات المخصصة أيضًا تعليقات توضيحية كاملة للأنواع، خاصةً عند استخدام العناصر العامة للسماح للمستدعين بتحديد أنواع البيانات:
import { useState, useEffect, useCallback } from 'react'
// Definition Hook Interface for Return Values
interface UseFetchResult<T> {
/** Return Data */
data: T | null
/** Loading... */
loading: boolean
/** Error Message */
error: string | null
/** Manually Resend Request */
refetch: () => void
}
// Generics Hook — Specified by the caller T Type
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const json: T = await response.json()
setData(json)
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
} finally {
setLoading(false)
}
}, [url])
useEffect(() => {
fetchData()
}, [fetchData])
return { data, loading, error, refetch: fetchData }
}
// --- Usage —— Type annotations are easy to understand at a glance ---
interface UserProfile {
id: number
name: string
email: string
avatar: string
}
function ProfilePage({ userId }: { userId: number }) {
// data Automatically inferred as UserProfile | null
const { data: user, loading, error, refetch } =
useFetch<UserProfile>(`/api/users/${userId}`)
if (loading) return <div>Loading......</div>
if (error) return <div style={{ color: 'red' }}>Error:{error}</div>
if (!user) return <div>No data</div>
return (
<div>
<img src={user.avatar} alt={user.name} width={64} />
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={refetch}>Refresh</button>
</div>
)
// ✅ user.name / user.email / user.avatar All have type hints
// ❌ user.phone Compilation errors occur(UserProfile Not included phone)
}
المبادئ الأساسية للتعليقات التوضيحية من نوع «Hook»:
- حدد قيمة الإرجاع على أنها
interface، وأضف تعليقات JSDoc إلى الحقول (سيعرضها المحرر تلقائيًا). useState<T | null>إعلام TypeScript بأنdataقد تكون قيمة فارغة، وبأنه يجب التحقق من أنها ليست قيمة فارغة عند استخدامها- يتم تمرير المعلمة العامة
<T>من قبل المُستدعي، وتؤديuseFetch<UserProfile>إلى تحويل جميع الحروف «T» الموجودة داخلuseFetchإلىUserProfile
(4) تقنيات الكتابة المتقدمة: العناصر الشرطية و«Omit»
نمط المتغيرات الشرطية
عندما يعتمد وجود عنصر من عناصر الدعم على عنصر آخر، يمكنك استخدام نمط «الاتحاد القابل للتعرف»:
// Regular Button vs Link Button — variant as 'link' Must Be Passed On href
type ButtonVariant =
| { variant: 'primary' | 'danger' | 'default' }
| { variant: 'link'; href: string; target?: '_blank' | '_self' }
interface SmartButtonProps {
label: string
} & ButtonVariant
function SmartButton(props: SmartButtonProps) {
if (props.variant === 'link') {
// Here props.href Type-safe existence
return <a href={props.href} target={props.target}>{props.label}</a>
}
return <button>{props.label}</button>
}
استخدم Omit لحذف الخصائص الأصلية غير الضرورية
import { ComponentPropsWithoutRef } from 'react'
// Custom Input Components,Pass-through is not allowed type(Force to text)
type CustomInputProps = Omit<
ComponentPropsWithoutRef<'input'>,
'type'
> & {
label: string
}
function CustomInput({ label, ...inputProps }: CustomInputProps) {
return (
<label>
{label}
<input type="text" {...inputProps} />
</label>
)
}
▶ المثال 5: المكونات العامة — جداول البيانات الآمنة من حيث النوع
interface Column<T> {
key: keyof T & string
title: string
render?: (value: T[keyof T], row: T) => React.ReactNode
}
function DataTable<T extends Record<string, any>>({ data, columns }: { data: T[]; columns: Column<T>[] }) {
return (
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
{columns.map(col => (
<th key={col.key} style={{ border: '1px solid #ddd', padding: 8, textAlign: 'left', background: '#f5f5f5' }}>
{col.title}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(col => (
<td key={col.key} style={{ border: '1px solid #ddd', padding: 8 }}>
{col.render ? col.render(row[col.key], row) : String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
interface User {
id: number
name: string
email: string
active: boolean
}
function UserTable() {
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@test.com', active: true },
{ id: 2, name: 'Bob', email: 'bob@test.com', active: false },
]
const columns: Column<User>[] = [
{ key: 'name', title: 'Name' },
{ key: 'email', title: 'Email' },
{ key: 'active', title: 'Status', render: (v) => (
<span style={{ color: v ? '#52c41a' : '#999' }}>{v ? 'Active' : 'Inactive'}</span>
)},
]
return <DataTable data={users} columns={columns} />
}
❓ أسئلة شائعة
interface وtype؟interface لتعريف المتغيرات المساعدة (props) والحالة (state) (يمكن إعلانهما معًا ويقدمان أداءً أفضل)؛ واستخدم type لتعريف أنواع الاتحاد (union types) والأنواع المتقاطعة (cross types) وأنواع الأدوات المساعدة (utility types) (على سبيل المثال، type Status = 'loading' | 'success' | 'error'). يمكن استبدال الاثنين ببعضهما في معظم الحالات، ولكن في المشاريع الجماعية، يُنصح بتوحيد استخدام أحدهما كخيار أساسي.React.FC لماذا لم يعد يُنصح باستخدامه؟React.FC (أو React.FunctionComponent) تتضمن الخاصية children بشكل افتراضي، ولكن في عملية التطوير الفعلية، لا تتطلب العديد من المكونات children، مما يؤدي إلى نوعية بيانات فضفاضة للغاية. بالإضافة إلى ذلك، لا يدعم React.FC المكونات العامة. وتتمثل أفضل الممارسات الحالية في المجتمع في إضافة تعليقات الأنواع مباشرةً إلى معلمات الدالة والتوقف عن استخدام React.FC.e.target وe.currentTarget؟e.currentTarget هو العنصر المرتبط بالحدث (آمن من حيث النوع)، بينما e.target هو العنصر الذي يُطلق الحدث فعليًّا (والذي قد يكون عنصرًا فرعيًّا). على سبيل المثال، إذا كان onChange مرتبطًا بـ input، فاستخدم e.currentTarget is always that input, but e.target could be an element inside the input. In TypeScript, you can use e.currentTarget للحصول على نوع العنصر الدقيق.extends لتقييد المعلمة العامة. على سبيل المثال، يضمن <T extends { id: string | number }> أن يتضمن T حقل id. إذا كان النوع الذي تم تمريره لا يحتوي على حقل id، فسيُصدر TypeScript خطأً.React.FC (أي React.FunctionComponent). والأسباب هي: ① إنه يضيف children?: ReactNode ضمناً، حتى لو لم يكن المكون الخاص بك بحاجة إلى عناصر فرعية؛ ② بناء الجملة العام معقد (React.FC<Props> مقابل ({ prop }: Props) => JSX.Element ببساطة)؛ ③ استنتاج الأنواع أقل دقة مع الصادرات الافتراضية مقارنةً بالتحديد الصريح لأنواع المعلمات. يُوصى بالتحديد الصريح لأنواع معلمات الدالة: function Comp({ name }: Props) {}.📖 ملخص
- يتم تعريف النوع
propsباستخدامinterface, andComponentPropsWithoutRefالذي يوسع نطاق سمات HTML الأصلية - يتيح المكون العام
<T>لمكونات الحاويات، مثل القوائم والجداول، معالجة أنواع متعددة من البيانات مع الحفاظ على أمان الأنواع. - أنواع أحداث React عامة:
ChangeEvent<T>،MouseEvent<T>، حيث T هو نوع العنصر - تستخدم الخطافات المخصصة أنواع إرجاع عامة لدعم استنتاج الأنواع؛ استخدم
interfaceلتعريف بنية نوع الإرجاع - تُعد «الركائز» (الاتحادات المميزة) و
Omitتقنيات متقدمة في مجال الأنواع تُستخدم للتحكم الدقيق في واجهات المكونات
📝 تمارين
- إنشاء مكون
Tableباستخدام TypeScript: وهو مكون عام<T extends { id: string | number }>يدعم تكوين الأعمدة (columns:{ key: keyof T, title: string }) ويُنفِّذ وظيفة الفرز. - إنشاء ربط مخصص
useLocalStorage<T>باستخدام TypeScript: ضمان سلامة الأنواع أثناء عمليات القراءة والكتابة، ودعم القيم الافتراضية، والتعامل تلقائيًا مع تسلسل JSON وإلغاء تسلسله. - إنشاء مكون
PasswordInputباستخدامOmitوComponentPropsWithoutRef: يرث هذا المكون جميع خصائص الإدخال الأصلية، لكنtypeيُعيَّن إلى"password"، كما تُضاف خاصيةshowToggleإضافية للتبديل بين إظهار كلمة المرور وإخفائها.