React: أفضل الممارسات في TypeScript و React

آخر تحديث: 2026-08-26

أثناء قيام فريق «توم» بتطوير وحدة الدفع، حدث خطأ فادح في بيئة الإنتاج: نظرًا لأن مكونًا معينًا كان يتوقع userId لكنه تلقى string من المصدر، فقد فشل طلب واجهة برمجة التطبيقات (API). ولو كان المشروع يستخدم لغة TypeScript، لكان قد تم الكشف عن عدم تطابق الأنواع هذا خلال مرحلة الترجمة. قرر «توم» اعتماد لغة TypeScript في المشروع بأكمله للكشف عن مثل هذه المشكلات في مرحلة مبكرة.


1. ما ستتعلمه



2. المخططات المفاهيمية

يوضح الرسم البياني التالي عملية التحقق من الأنواع التي يقوم بها TypeScript في تدفق البيانات لمكون React:

100%
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[] }

يتضمن مشروع الدفع الخاص بـ«توم» تدفق البيانات التالي:

TEXT 📖 للعرض فقط
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 لتعريف أنواع الاتحاد/أنواع المساعدة.

التعريفات الأساسية للمصطلحات

TSX
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 لورثها:

TSX
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: مكون قائمة عامة

TSX
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>
      )}
    />
  )
}

الآليات الرئيسية للأدوية الجنيسة:

  1. T في ListProps<T> هو «معلمة نوع» يتم استنتاجها تلقائيًا بواسطة TypeScript عند استخدامها.
  2. عند تمرير items={users} (من النوع User[])، يصبح item في renderItem تلقائيًا من النوع User
  3. يتغير 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: استكمال نموذج البحث عن نوع الحدث

TSX
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

النقاط الرئيسية بشأن أنواع الفعاليات:

  1. React.ChangeEvent<HTMLInputElement> — المعلمة العامة هي نوع العنصر المرتبط بالحدث، وهي التي تحدد أنواع e.target وe.currentTarget
  2. e.currentTarget هو العنصر المرتبط بالحدث (آمن من حيث النوع)، وe.target هو العنصر الذي يُطلق الحدث فعليًّا (والذي قد يكون عنصرًا تابعًا)
  3. تُرجع الدالة KeyboardEvent إلى e.key سلسلة نصية؛ ولا يلزم تحديد نوع إضافي

▶ المثال 4: تعليقات الأنواع للـ«هوكات» المخصصة

تتطلب الخطافات المخصصة أيضًا تعليقات توضيحية كاملة للأنواع، خاصةً عند استخدام العناصر العامة للسماح للمستدعين بتحديد أنواع البيانات:

TSX
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»:

  1. حدد قيمة الإرجاع على أنها interface، وأضف تعليقات JSDoc إلى الحقول (سيعرضها المحرر تلقائيًا).
  2. useState<T | null> إعلام TypeScript بأن data قد تكون قيمة فارغة، وبأنه يجب التحقق من أنها ليست قيمة فارغة عند استخدامها
  3. يتم تمرير المعلمة العامة <T> من قبل المُستدعي، وتؤدي useFetch<UserProfile> إلى تحويل جميع الحروف «T» الموجودة داخل useFetch إلى UserProfile

(4) تقنيات الكتابة المتقدمة: العناصر الشرطية و«Omit»

نمط المتغيرات الشرطية

عندما يعتمد وجود عنصر من عناصر الدعم على عنصر آخر، يمكنك استخدام نمط «الاتحاد القابل للتعرف»:

TSX
// 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 لحذف الخصائص الأصلية غير الضرورية

TSX
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: المكونات العامة — جداول البيانات الآمنة من حيث النوع

TSX
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 يوصي باستخدام React.FC (أي React.FunctionComponent). والأسباب هي: ① إنه يضيف children?: ReactNode ضمناً، حتى لو لم يكن المكون الخاص بك بحاجة إلى عناصر فرعية؛ ② بناء الجملة العام معقد (React.FC<Props> مقابل ({ prop }: Props) => JSX.Element ببساطة)؛ ③ استنتاج الأنواع أقل دقة مع الصادرات الافتراضية مقارنةً بالتحديد الصريح لأنواع المعلمات. يُوصى بالتحديد الصريح لأنواع معلمات الدالة: function Comp({ name }: Props) {}.

📖 ملخص


📝 تمارين

  1. إنشاء مكون Table باستخدام TypeScript: وهو مكون عام <T extends { id: string | number }> يدعم تكوين الأعمدة (columns: { key: keyof T, title: string }) ويُنفِّذ وظيفة الفرز.
  2. إنشاء ربط مخصص useLocalStorage<T> باستخدام TypeScript: ضمان سلامة الأنواع أثناء عمليات القراءة والكتابة، ودعم القيم الافتراضية، والتعامل تلقائيًا مع تسلسل JSON وإلغاء تسلسله.
  3. إنشاء مكون PasswordInput باستخدام Omit وComponentPropsWithoutRef: يرث هذا المكون جميع خصائص الإدخال الأصلية، لكن type يُعيَّن إلى "password"، كما تُضاف خاصية showToggle إضافية للتبديل بين إظهار كلمة المرور وإخفائها.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%