React مع TypeScript يعني كتابة المكوّنات في ملفات .tsx حيث لـ props والحالة والمراجع ومعالجات الأحداث أنواع، فيلتقط المترجم prop مفقودة أو معالج حدث خاطئًا قبل أن يعمل الكود. وتختفي الأنواع وقت البناء: ما يعمل في المتصفح هو JavaScript نفسها التي كنت ستكتبها من دونها. تشغّل المحررات الحية في هذه الصفحة تلك JavaScript، وتجلس النسخة ذات الأنواع بجانب كل منها في كتلة ثابتة.
إليك Badge نفسه مع الأنواع. count اختيارية (?)، فتعود الشارة الثانية إلى القيمة الافتراضية 0، وتقبل children عنصرًا ونصًا عاديًا كليهما.
type BadgeProps = {
label: string;
count?: number;
children: React.ReactNode;
};
function Badge({ label, count = 0, children }: BadgeProps) {
return (
<div>
<strong>{label}</strong> ({count})
<div>{children}</div>
</div>
);
}
<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'
إنشاء مشروع TypeScript
لدى Vite قالب لـ React مع TypeScript:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
تحصل على ملفات .tsx، وملف tsconfig.json فيه strict مفعّل، وتعريفات أنواع React (@types/react و@types/react-dom) مثبتة أصلًا. يحذف Vite الأنواع أثناء التقديم والبناء لكنه لا يفحصها؛ يشغّل سكربت build في القالب tsc أولًا، ويفحص محررك أثناء الكتابة. وتُعدّ أطر العمل مثل Next.js لغة TypeScript بالطريقة نفسها عندما تنشئ مشروعًا بأداة سطر الأوامر الخاصة بها.
تحديد أنواع props
صِف props كنوع كائن وعلّق على المعامل المفكك. بضعة أنماط تغطي معظم المكوّنات:
interface UserCardProps {
name: string; // required
age?: number; // optional, may be undefined
role: 'admin' | 'member'; // a union of allowed strings
tags: string[];
onSelect: (id: string) => void; // a callback prop
children?: React.ReactNode; // anything React can render
}
function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
// ...
}
type أم interface. كلاهما يعمل لـ props. يمكن توسيع interface بـ extends ودمجه بإعلانه مرتين؛ ويمكن لـ type أيضًا التعبير عن الاتحادات والتقاطعات والأنواع المعيّنة. تختار معظم الفرق واحدًا وتستخدمه في كل مكان.
children. استخدم React.ReactNode لكل ما يوضع بين الوسمين: العناصر والنصوص والأرقام والمصفوفات وnull. تشرح صفحة props كيف تصل children أصلًا.
لماذا React.FC اختياري. يحدد const Badge: React.FC<BadgeProps> = (...) => ... نوع الدالة كلها بدلًا من المعامل. ومنذ أنواع React 18 لم يعد يضيف children نيابة عنك، ولا يستطيع أخذ معامل نوع لمكوّن عام، ولا يضيف شيئًا لا يضيفه معامل محدد النوع. يستخدمه الكود الأقدم على نطاق واسع؛ أما الدوال العادية مع props محددة الأنواع فهي الخيار الشائع للكود الجديد.
تحديد نوع useState
لقيمة ابتدائية بسيطة يكفي الاستنتاج: useState(0) من نوع number، وuseState('') من نوع string. مرّر وسيط نوع عندما يمكن أن تحمل الحالة أكثر مما توحي به قيمتها الابتدائية: قيمة تبدأ null، أو واحد من عدة نصوص ثابتة.
انقر "Load user" فتنتقل الحالة من idle إلى loading إلى done. والحالة محددة النوع:
type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');
user.name; // Error: 'user' is possibly 'null'
user?.name; // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'
من دون <User | null> يستنتج useState(null) النوع null ويفشل setUser(data) في الترجمة. ويجبرك الاتحاد على التعامل مع الحالة الفارغة، وهو بالضبط الفحص user ? ... : ... في المثال العامل.
useRef لعناصر DOM
المرجع الذي يشير إلى عنصر DOM يأخذ نوع العنصر ويبدأ null، لأن العنصر لا يوجد حتى تربطه React بعد العرض الأول.
اكتب شيئًا، ثم انقر "Log the value": تطبع وحدة التحكم ما كتبته، مقروءًا مباشرة من عنصر DOM. والنسخة محددة النوع:
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null
inputRef.current?.focus(); // OK
inputRef.current.focus(); // Error: 'inputRef.current' is possibly 'null'
const timerId = useRef<number | null>(null); // a mutable value, not a DOM node
استخدم واجهة العنصر التي تطابق الوسم: HTMLInputElement وHTMLDivElement وHTMLCanvasElement وHTMLButtonElement. وفي أنواع React 19 يأخذ useRef دائمًا وسيطًا ويُرجع RefObject يمكنك الإسناد إلى current الخاصة به، فيعمل الشكل نفسه لمراجع DOM وللقيم مثل معرّف مؤقت. راجع useRef لجانب وقت التشغيل.
أنواع معالجات الأحداث
المعالجات المضمّنة لا تحتاج إلى تعليق نوع: في onChange={(e) => setName(e.target.value)} يعرف TypeScript نوع e من الـ prop الممرر إليها. وعلّق عندما تنقل معالجًا إلى دالته الخاصة.
أرسل فتسجّل وحدة التحكم العنوان؛ ويمنع preventDefault إعادة تحميل الصفحة. ومع الأنواع:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}
function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') { /* ... */ }
}
منذ @types/react 19.2.10، يُحدد نوع onSubmit بـ React.SubmitEvent ويُعلَّم React.FormEvent الأقدم كمُهمل؛ ومع إصدار أقدم من الأنواع، اكتب React.FormEvent<HTMLFormElement> بدلًا منه. معامل النوع هو العنصر المرتبط به المعالج. وإذا لم تكن متأكدًا من النوع، فمرّر المؤشر فوق الـ prop المسماة onChange في محررك، أو حدد نوع المعالج كله دفعة واحدة: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.
تحديد نوع السياق
أعطِ createContext نوع القيمة. إذا وُجدت قيمة افتراضية معقولة فمررها، فيحصل كل مستهلك على قيمة غير فارغة:
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
const theme = useContext(ThemeContext); // Theme
وعندما لا توجد قيمة افتراضية جيدة (مستخدم مسجّل، أو مخزن)، ابدأ بـ null وغلّف useContext في خطاف يرمي خطأ إذا استُخدم مكوّن خارج المزوّد. عندها يحصل كل مستدعٍ على النوع غير الفارغ ورسالة خطأ واضحة بدلًا من انهيار في العمق.
type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);
export function useAuth(): Auth {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
return value;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState('Ada');
return (
<AuthContext value={{ user, logout: () => setUser('') }}>
{children}
</AuthContext>
);
}
<AuthContext value={...}> هي صيغة المزوّد في React 19؛ ويُحدد نوع <AuthContext.Provider value={...}> بالطريقة نفسها.
المكوّنات العامة
القائمة أو الجدول أو قائمة الاختيار التي تعمل مع أي نوع من العناصر مكوّن عام (generic): يربط معامل نوع الـ prop المسماة items بدالة الاستدعاء renderItem.
المكوّن List محدد النوع:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
getKey: (item: T) => string | number;
};
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error
يستنتج TypeScript النوع T من items، فلا يكتب المستدعون معامل النوع أبدًا. وفي ملف .tsx تحتاج الدالة السهمية إلى <T,> بدلًا من <T>، لأن <T> وحدها تُقرأ كوسم JSX.
توسيع props العناصر الأصلية
المغلّف حول button أو input ينبغي أن يقبل كل سمة يقبلها العنصر الأصلي. React.ComponentProps<'button'> هي تلك المجموعة الكاملة؛ أضف props الخاصة بك فوقها وانشر الباقي.
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'ghost';
};
function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
return (
<button
{...rest}
style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
/>
);
}
<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
Save
</Button>;
أنواع onClick وdisabled وtype وaria-* كلها محددة دون سردها. وفي React 19 تكون ref prop عادية، ويتضمنها ComponentProps<'button'>، فيصل <Button ref={buttonRef}> إلى عقدة DOM عبر النشر دون forwardRef. استخدم ComponentPropsWithoutRef<'button'> عندما تريد استبعاد ref، وComponentProps<typeof UserCard> لقراءة props أحد مكوّناتك الخاصة.
أخطاء أنواع شائعة
"'ref.current' is possibly 'null'". العنصر غير موجود أثناء العرض الأول. استخدم ref.current?.focus()، أو افحص if (ref.current) قبل استخدامه.
"Type 'string' is not assignable" في حالة من نوع اتحاد. يستنتج const [status, setStatus] = useState('idle') النوع string، وهو أوسع مما تريد، وتمريره إلى prop نوعها Status يفشل. اكتب useState<Status>('idle').
حدث محدد النوع بالعنصر الخطأ. React.ChangeEvent<HTMLInputElement> على select يجعل e.target من النوع الخطأ. يجب أن يطابق معامل النوع العنصر المرتبط به المعالج.
استخدام any لإسكات خطأ. يطفئ الفحص لكل ما تلمسه تلك القيمة. فضّل unknown وضيّقه، أو أصلح النوع من مصدره.
الأسئلة الشائعة
كيف أنشئ تطبيق React مع TypeScript؟
شغّل npm create vite@latest my-app -- --template react-ts، ثم npm install وnpm run dev. توضع المكوّنات في ملفات .tsx ويحذف Vite الأنواع أثناء البناء؛ وشغّل tsc (يفعل سكربت build في القالب ذلك) لفحصها.
ما نوع children في React؟
React.ReactNode. يقبل كل ما يمكن لـ React عرضه: العناصر والنصوص والأرقام والمصفوفات وnull وundefined. ولا تستخدم React.ReactElement إلا عندما تحتاج إلى عنصر واحد بالضبط.
كيف أحدد نوع useState مع null؟
مرّر النوع صراحة كاتحاد: useState<User | null>(null). من دون وسيط النوع يستنتج TypeScript أن null هي القيمة الوحيدة المسموح بها.
ما نوع حدث onChange في React؟
React.ChangeEvent<HTMLInputElement> لحقل إدخال (وHTMLTextAreaElement أو HTMLSelectElement لتلك العناصر). عندها يكون نوع e.target.value هو string.
هل أستخدم React.FC؟
هو اختياري. تحديد نوع معامل props مباشرة (function Card({ title }: CardProps)) يؤدي المهمة نفسها، وأبسط قراءة، ويعمل مع الأنواع العامة. لم يعد React.FC يضيف children ضمنيًا، فلا يعطيك إلا القليل.
هل أستخدم type أم interface لـ props؟
كلاهما يعمل. يمكن توسيع interface ودمجه؛ ويمكن لـ type التعبير عن الاتحادات والأنواع المعيّنة (mapped types). اختر اصطلاحًا واحدًا للمشروع والتزم به.