استخدام TypeScript مع تطبيقات React
في عالم تطوير الواجهات الأمامية، كثيرًا ما يبدأ المشروع بحماس كبير، ثم يكبر شيئًا فشيئًا حتى يصبح من الصعب تتبع كل التفاصيل الصغيرة فيه. قد تبدأ بمكون بسيط يعرض بيانات من API، ثم تضيف نموذج تسجيل، ثم صفحات متعددة، ثم إدارة حالة، ثم صلاحيات، ثم اختبارات، ثم يأتي اليوم الذي تفتح فيه ملفًا قديمًا وتقول في نفسك: “كيف كان هذا الجزء يعمل أصلًا؟”. هنا بالضبط يظهر دور TypeScript مع React ليس كترف تقني، بل كطبقة حماية ووضوح وراحة نفسية أثناء التطوير.
React يمنحك مرونة هائلة في بناء الواجهات التفاعلية، لكن هذه المرونة نفسها قد تتحول إلى فوضى إذا لم تكن هناك حدود واضحة للبيانات والوظائف. TypeScript يأتي ليضع هذه الحدود بلغة مفهومة ومباشرة: ما نوع البيانات التي تتوقعها؟ ما الذي يمكن أن يصل إلى هذا المكون؟ ما الذي يمكن أن يعيده هذا الـ hook؟ وما الذي يجب أن يحدث لو كانت القيمة null أو undefined؟ هذه الأسئلة ليست نظرية، بل هي فرق بين مشروع يمكن الحفاظ عليه لسنوات ومشروع ينكسر مع أول توسيع حقيقي.
حين تستخدم TypeScript مع React، فأنت لا تضيف مجرد “typing” إلى الكود، بل تعيد ترتيب طريقة التفكير نفسها. تصبح المكونات أوضح، والـ props أكثر صراحة، والـ state أقل غموضًا، والتعامل مع الـ API أكثر أمانًا. والأجمل من ذلك أن هذا الوضوح لا يأتي على حساب السرعة، بل غالبًا يزيدها؛ لأنك تتخلص من كثير من الأخطاء الصغيرة قبل أن تتحول إلى مشاكل في المتصفح أو في الإنتاج.
لماذا TypeScript مع React؟
السؤال الذي يطرحه كثير من المطورين في البداية هو: لماذا أُتعب نفسي بإضافة TypeScript إلى React بينما React تعمل بدون ذلك؟ والجواب العملي هو أن المشاريع الصغيرة جدًا قد تبدو بخير بدون typing، لكن أي مشروع فيه فريق، أو تكاملات كثيرة، أو نمو مستمر، سيبدأ بسرعة في إظهار قيمة TypeScript.
أول فائدة واضحة هي اكتشاف الأخطاء مبكرًا. بدل أن تكتشف أنك مررت قيمة string إلى مكون ينتظر number أثناء تشغيل التطبيق، سيخبرك TypeScript مباشرة أثناء الكتابة أو أثناء الـ build. هذا النوع من الاكتشاف المبكر يختصر ساعات من التتبع، خاصة عندما تكون المشكلة ناتجة عن سلسلة طويلة من props أو state أو data coming from the server.
الفائدة الثانية هي تحسين تجربة التطوير نفسها. عندما تكتب اسم prop داخل المكون، سيُظهر لك المحرر أسماءها وأنواعها. وعندما تستدعي hook أو function، ستظهر لك التوقيعات الصحيحة وما الذي ترجع إليه. هذه ليست تفاصيل صغيرة، بل فرق كبير في السرعة الذهنية: بدل أن تتذكر كل شيء، يصبح المحرر شريكًا ذكيًا يذكرك.
الفائدة الثالثة هي سهولة الصيانة. بعد عدة أشهر، قد لا تتذكر لماذا أضفت حقلاً معينًا أو لماذا كان هذا النوع يسمح فقط ببعض القيم. TypeScript يجعل هذه القرارات جزءًا من الكود نفسه. ومع الوقت، يصبح الكود أكثر توثيقًا من كونه مجرد تنفيذ.
الفائدة الرابعة هي أفضلية واضحة في الفرق الكبيرة. حين يعمل أكثر من شخص على نفس الواجهة، تصبح العقود بين المكونات واضحة. كل فرد يعرف ما الذي يتوقعه المكون وما الذي يخرجه. وهذا يقلل سوء الفهم، ويجعل refactoring أكثر أمانًا، ويخفف من الاعتماد على التفاهمات الشفوية التي تُنسى بسرعة.
تجهيز مشروع React مع TypeScript
إذا كنت تبدأ مشروعًا جديدًا، فالأمر أصبح أسهل بكثير مما كان عليه سابقًا. أغلب الأدوات الحديثة تدعم TypeScript من البداية. ومع Vite مثلًا، يمكن إنشاء مشروع React + TypeScript بسرعة كبيرة.
npm create vite@latest my-react-ts-app -- --template react-ts
cd my-react-ts-app
npm install
npm run dev
أو باستخدام Create React App في المشاريع القديمة نسبيًا:
npx create-react-app my-react-ts-app --template typescript
في المشاريع الموجودة أصلًا بلغة JavaScript، يمكنك الانتقال تدريجيًا إلى TypeScript. هذا انتقال شائع جدًا، وليس مطلوبًا أن تُحوّل كل شيء في يوم واحد. أحيانًا يكون الأفضل أن تبدأ بملف أو اثنين، ثم تكمل تدريجيًا. الفكرة ليست “التحول الكامل الآن”، بل بناء طبقة TypeScript بشكل ذكي تحترم الواقع العملي للمشروع.
فهم الملفات في React + TypeScript
أول تغيير ستلاحظه هو امتداد الملفات. بدل .js أو .jsx ستبدأ برؤية .ts و.tsx. الفرق بسيط لكنه مهم:
.tsلملفات TypeScript العادية..tsxلملفات TypeScript التي تحتوي على JSX.
هذا يعني أن أي مكون React يعرض JSX يجب أن يكون غالبًا بامتداد .tsx.
مثال بسيط:
type GreetingProps = {
name: string;
};
function Greeting({ name }: GreetingProps) {
return <h1>مرحبًا، {name}</h1>;
}
export default Greeting;
هنا لا يوجد شيء معقد، لكن القيمة الحقيقية تظهر بسرعة: المكون يعرف بوضوح أنه ينتظر name من نوع string. إذا حاولت تمرير رقم أو قيمة ناقصة، سيتدخل TypeScript مباشرة.
كتابة أول مكون مضبوط بالأنواع
لنأخذ مثالًا عمليًا أكثر قربًا من الواقع. افترض أنك تبني بطاقة منتج في متجر إلكتروني.
type ProductCardProps = {
id: number;
title: string;
price: number;
inStock: boolean;
};
function ProductCard({ title, price, inStock }: ProductCardProps) {
return (
<div className="product-card">
<h2>{title}</h2>
<p>السعر: {price} درهم</p>
<p>{inStock ? "متوفر" : "غير متوفر"}</p>
</div>
);
}
export default ProductCard;
عندما تبدأ بكتابة هذا المكون، قد يبدو بسيطًا جدًا، لكن فكر في مدى سهولة استخدامه بعد ذلك داخل صفحات متعددة. كل مكون يحمل معه وثيقة مصغرة عن نفسه. لا تحتاج إلى فتح ملف آخر أو قراءة تعليق قديم لتعرف المدخلات المطلوبة.
ولو حاولت استخدامه هكذا:
<ProductCard title="هاتف ذكي" price="1200" inStock={true} />
فستلاحظ فورًا أن price يجب أن يكون رقمًا وليس نصًا. هذا النوع من الخطأ قد يمر أحيانًا في JavaScript إلى أن يتسبب في سلوك غريب عند التنسيق أو الحسابات. TypeScript يمنعه من البداية.
أهم المفاهيم التي تحتاجها في React مع TypeScript
ليس مطلوبًا أن تصبح خبيرًا في كل تفاصيل TypeScript لكي تستفيد منه مع React. هناك مجموعة مفاهيم أساسية، وإذا فهمتها جيدًا ستغطي نسبة كبيرة جدًا من الاستخدام اليومي.
1) Type aliases و Interfaces
كلاهما يُستخدم لتعريف شكل البيانات. غالبًا سترى الاثنين في مشاريع React. من الناحية العملية، كثير من الفرق تستخدم type للـ props والـ state والـ unions، بينما تستخدم interface عندما تريد تعريف عقد بيانات قابل للتوسعة.
مثال باستخدام type:
type User = {
id: number;
name: string;
email: string;
};
ومثال باستخدام interface:
interface User {
id: number;
name: string;
email: string;
}
في React، الاختيار بينهما غالبًا ليس معركة فلسفية. الأهم هو الاتساق داخل المشروع. اختر نمطًا واضحًا واتبعْه.
2) Union types
واحدة من أقوى ميزات TypeScript هي أن بعض القيم يمكن أن تكون واحدة من عدة أنواع. هذا مفيد جدًا في React.
type Status = "idle" | "loading" | "success" | "error";
هذا التعريف يمنعك من استخدام أي قيمة خارج هذه الخيارات. جميل، أليس كذلك؟ بدل أن تكتب نصوصًا عشوائية وتنتظر حدوث الخطأ أثناء التشغيل، يصبح الحالة واضحة ومحددة.
3) Optional props
ليس كل prop يجب أن يكون إلزاميًا.
type ButtonProps = {
label: string;
variant?: "primary" | "secondary";
};
إذا لم يُمرر variant، يمكن للمكون استخدام قيمة افتراضية.
function Button({ label, variant = "primary" }: ButtonProps) {
return <button className={variant}>{label}</button>;
}
4) Generics
قد تبدو generics مخيفة في البداية، لكنها مفيدة جدًا، خاصة مع البيانات العامة، القوائم، والـ hooks القابلة لإعادة الاستخدام.
سنعود إليها بتفصيل لاحقًا، لكن الفكرة الأساسية أنها تسمح لك بكتابة كود مرن دون أن تفقد قوة التحليل النوعي.
props في React مع TypeScript
الـ props هي أول مكان يشعر فيه المطور بقوة TypeScript. لأنها ببساطة العقد الأساسي بين المكونات. المكون الذي لا يعرف ما يستقبله سيصبح عرضة للارتباك، أما عندما تُعرّف props بشكل واضح، فإنك تحول المكون إلى وحدة يمكن الوثوق بها.
مثال مكون مستخدم في واجهة مقال:
type ArticleHeaderProps = {
title: string;
author: string;
publishedAt: string;
readingTime?: number;
};
function ArticleHeader({
title,
author,
publishedAt,
readingTime,
}: ArticleHeaderProps) {
return (
<header>
<h1>{title}</h1>
<p>بقلم {author}</p>
<p>تاريخ النشر: {publishedAt}</p>
{readingTime && <p>مدة القراءة: {readingTime} دقيقة</p>}
</header>
);
}
هنا نلاحظ أمرًا مهمًا: readingTime اختياري. لكن حتى مع كونه اختياريًا، TypeScript يجعلك تتعامل معه بوعي. لا تفترض أنه موجود دائمًا.
في الاستخدام:
<ArticleHeader
title="Using TypeScript with React Applications"
author="Agmir Hassan"
publishedAt="2026-06-14"
readingTime={8}
/>
والفائدة الحقيقية لا تظهر فقط في كتابة الكود، بل في إعادة استخدامه. حين تعود إلى هذا المكون لاحقًا، ستعرف فورًا ما يحتاجه وما يمكن تجاهله.
state في React مع TypeScript
الـ state هو مكان آخر يزداد فيه الغموض عندما تكون البيانات متنوعة أو متغيرة. وهنا TypeScript يساعدك كثيرًا.
لنأخذ مثالًا بسيطًا باستخدام useState.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState<number>(0);
return (
<div>
<p>العدد: {count}</p>
<button onClick={() => setCount(count + 1)}>زيادة</button>
</div>
);
}
لاحظنا أننا مررنا <number> إلى useState. أحيانًا TypeScript يستنتج النوع تلقائيًا، لكن في الحالات الأكثر تعقيدًا يكون التصريح صريحًا أفضل.
مثال لقيمة قد تكون فارغة في البداية:
const [username, setUsername] = useState<string>("");
ومثال لبيانات قد تكون null إلى أن تصل من API:
type User = {
id: number;
name: string;
};
const [user, setUser] = useState<User | null>(null);
هنا TypeScript يجبرك على التفكير: هل user موجود أم لا؟ هذا التفكير مهم جدًا، لأنه يمنعك من كتابة شيء مثل user.name قبل التأكد من وجود user.
if (!user) {
return <p>جارٍ تحميل المستخدم...</p>;
}
return <p>مرحبًا {user.name}</p>;
هذا النمط شائع جدًا، وهو من أجمل الفوائد العملية لـ TypeScript مع React. بدل أن تتعامل مع الاحتمالات في رأسك فقط، يصبح الكود نفسه مرشدًا لك.
التعامل مع الأحداث Events
أحد أكثر المواضع التي تسبب ارتباكًا للمبتدئين هو typing للأحداث.
في JavaScript قد تكتب:
function handleChange(event) {
console.log(event.target.value);
}
لكن مع TypeScript، الأفضل أن تعرف نوع الحدث بوضوح.
import { ChangeEvent, useState } from "react";
function SearchBox() {
const [query, setQuery] = useState("");
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
};
return <input type="text" value={query} onChange={handleChange} />;
}
الآن TypeScript يعرف أن الحدث قادم من input، وبالتالي event.target.value مفهوم وآمن.
مثال على زر:
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
console.log("تم الضغط على الزر");
}
وفي نموذج:
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
console.log("تم الإرسال");
}
هذه التفاصيل الصغيرة ترفع مستوى الثقة في الكود بشكل واضح. بدل أن تتعامل مع event عام وغامض، يصبح كل شيء محددًا.
استخدام Types مع القوائم والبيانات المكررة
غالبًا ستتعامل مع arrays من الكائنات، خصوصًا عند عرض بيانات من API أو من state محلي.
type Todo = {
id: number;
text: string;
completed: boolean;
};
type TodoListProps = {
items: Todo[];
};
function TodoList({ items }: TodoListProps) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.completed ? "✅" : "⬜"} {item.text}
</li>
))}
</ul>
);
}
هذا المثال بسيط، لكنه يوضّح قيمة typing بوضوح: كل عنصر له id وtext وcompleted. لا يوجد مجال للفوضى. ولو حاولت تمرير عنصر ناقص، ستعرف ذلك قبل التشغيل.
التعامل مع API Responses
أحد أهم أسباب استخدام TypeScript مع React هو تحسين التعامل مع البيانات القادمة من API. لأن البيانات الخارجية دائمًا هي أكثر الأماكن عرضة للمفاجآت. قد تكون القيمة فارغة، أو الحقل غير موجود، أو الشكل مختلفًا عما كنت تتوقع.
لنفترض أن لديك endpoint يرجع مستخدمين:
type ApiUser = {
id: number;
name: string;
email: string;
};
async function fetchUsers(): Promise<ApiUser[]> {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
return response.json();
}
ثم تستخدمها داخل component:
import { useEffect, useState } from "react";
type ApiUser = {
id: number;
name: string;
email: string;
};
function UsersPage() {
const [users, setUsers] = useState<ApiUser[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadUsers() {
try {
const data = await fetchUsers();
setUsers(data);
} finally {
setLoading(false);
}
}
loadUsers();
}, []);
if (loading) {
return <p>جارٍ التحميل...</p>;
}
return (
<div>
{users.map((user) => (
<p key={user.id}>
{user.name} — {user.email}
</p>
))}
</div>
);
}
لكن هنا ينبغي الانتباه: TypeScript يثق بما تقوله له، وليس بما يرسله الخادم فعلًا. إذا كنت تريد أمانًا أعلى، فهناك طبقة إضافية مهمة جدًا: validation. لأن typing وحده لا يضمن أن الـ API لم يرسل بيانات غير صحيحة. لذلك في التطبيقات الحقيقية، من الأفضل أحيانًا الجمع بين TypeScript وruntime validation مثل Zod أو Yup.
مثال باستخدام Zod:
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type ApiUser = z.infer<typeof UserSchema>;
ثم تتحقق من البيانات بعد الاستلام:
const result = UserSchema.array().safeParse(data);
if (!result.success) {
throw new Error("Invalid user data");
}
setUsers(result.data);
هذا النمط ممتاز عندما يكون الأمان مهمًا، أو عندما تأتي البيانات من مصادر متعددة، أو عندما لا تثق تمامًا بشكل response.
React Hooks مع TypeScript
الـ hooks أصبحت جزءًا أساسيًا من React الحديثة، وTypeScript يتناغم معها بشكل ممتاز.
useState
شرحناه بالفعل، لكن من المفيد أن نتذكر أن النوع يمكن أن يكون واضحًا جدًا عندما تكون القيم محتملة التغير أو البداية الفارغة.
const [isOpen, setIsOpen] = useState<boolean>(false);
const [items, setItems] = useState<string[]>([]);
useEffect
عادة لا يحتاج useEffect إلى نوع خاص، لكن البيانات التي يتعامل معها قد تحتاج typing واضح.
useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(timer);
}, []);
useRef
useRef من الأماكن التي يخطئ فيها المبتدئون كثيرًا، خصوصًا عند التعامل مع عناصر DOM.
import { useEffect, useRef } from "react";
function InputFocus() {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
هنا نرى أمرًا جميلًا جدًا: current قد تكون null في البداية، لذلك نعرّفها بوضوح. وعندما تريد استدعاء focus، تستخدم optional chaining لتجنب الأخطاء.
useReducer
useReducer يصبح مفيدًا جدًا في الحالات الأكثر تعقيدًا. مع TypeScript، يمكن أن يصبح قويًا جدًا وشفافًا.
type State = {
count: number;
};
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" };
const initialState: State = {
count: 0,
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return initialState;
default:
return state;
}
}
هذا المثال يوضح إحدى أجمل خصائص TypeScript: Action هنا اتحاد من عدة أشكال، وكل حالة في reducer تصبح واضحة ومقصودة. إذا نسيت حالة، أو كتبت type خاطئًا، فسيُنبّهك المحرر.
Typing للمكونات القابلة لإعادة الاستخدام
من أفضل الاستثمارات التي تقوم بها في مشروع React كبير هي بناء مكونات قابلة لإعادة الاستخدام بأنواع واضحة جدًا. لأن هذه المكونات تصبح لاحقًا أساسًا لصفحات كثيرة.
مكون Button
type ButtonProps = {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
variant?: "primary" | "secondary" | "danger";
};
function Button({
children,
onClick,
disabled = false,
variant = "primary",
}: ButtonProps) {
return (
<button
disabled={disabled}
onClick={onClick}
className={`btn btn-${variant}`}
>
{children}
</button>
);
}
هذا المكون مرن، لكن ليس عشوائيًا. كل خيار مقصود. وهنا تظهر قيمة TypeScript في ضبط واجهات الاستخدام.
مكون Modal
type ModalProps = {
isOpen: boolean;
title: string;
onClose: () => void;
children: React.ReactNode;
};
function Modal({ isOpen, title, onClose, children }: ModalProps) {
if (!isOpen) return null;
return (
<div className="modal">
<div className="modal-content">
<header>
<h2>{title}</h2>
<button onClick={onClose}>إغلاق</button>
</header>
<div>{children}</div>
</div>
</div>
);
}
بمجرد أن ترى تعريف ModalProps، ستفهم تمامًا كيف يستخدم هذا المكون.
TypeScript مع Forms في React
النماذج من أكثر الأماكن التي يظهر فيها الخطأ إذا لم تكن الأنواع واضحة. لأن الحقول تختلف، والتحقق يزداد، والتعامل مع القيم الفارغة يصبح يوميًا.
مثال form بسيط:
import { FormEvent, useState } from "react";
type ContactFormData = {
name: string;
email: string;
message: string;
};
function ContactForm() {
const [formData, setFormData] = useState<ContactFormData>({
name: "",
email: "",
message: "",
});
const handleChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = event.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input name="name" value={formData.name} onChange={handleChange} />
<input name="email" value={formData.email} onChange={handleChange} />
<textarea
name="message"
value={formData.message}
onChange={handleChange}
/>
<button type="submit">إرسال</button>
</form>
);
}
لاحظ هنا أننا استخدمنا HTMLInputElement | HTMLTextAreaElement لأن الحقل قد يكون input أو textarea. هذه التفاصيل النوعية تساعد في بناء forms مرنة من دون فقدان السلامة.
لكن هناك نقطة مهمة: عندما تستخدم [name]: value، قد تحتاج في المشاريع الأكبر إلى typing أكثر صرامة لضمان أن name لا يقبل إلا الحقول المصرح بها. ويمكن فعل ذلك باستخدام keyof، وهي ميزة مفيدة جدًا.
keyof وtypeof في المشاريع العملية
هذه الأدوات قد تبدو نظرية في البداية، لكنها مفيدة جدًا في الواقع.
keyof
type User = {
id: number;
name: string;
email: string;
};
type UserKey = keyof User;
الآن UserKey يمكن أن يكون فقط "id" | "name" | "email".
هذا مفيد جدًا في الدوال العامة:
function getUserValue(user: User, key: keyof User) {
return user[key];
}
typeof
يمكنك أحيانًا استخراج النوع من قيمة موجودة بدل إعادة كتابته.
const statusMap = {
idle: "جاهز",
loading: "جارٍ التحميل",
success: "تم بنجاح",
error: "حدث خطأ",
};
type StatusKey = keyof typeof statusMap;
هذا النمط رائع عندما تكون البيانات ثابتة نسبيًا وتريد أن تظل الأنواع متزامنة مع القيم.
التعامل مع null و undefined بشكل صحيح
من أكثر الفوائد التي ستلاحظها بعد الانتقال إلى TypeScript أنك ستصبح أكثر وعيًا بالقيم غير الموجودة. وهذا ليس تعقيدًا زائدًا، بل تدريب جيد على الكتابة الآمنة.
مثال:
type Profile = {
bio?: string;
};
function ProfileCard({ bio }: Profile) {
return <p>{bio ?? "لا توجد نبذة حالياً"}</p>;
}
استخدمنا ?? بدل || لأننا نريد فقط استبدال القيم null أو undefined، وليس كل قيمة “falsy”.
ومثال آخر:
function renderTitle(title: string | null) {
if (!title) {
return "عنوان غير متوفر";
}
return title;
}
في كثير من الأحيان، TypeScript يدفعك لكتابة checks أفضل. وهذا قد يبدو مزعجًا في البداية، لكنه في الحقيقة يجعل الكود أقرب إلى واقع البيانات.
أنماط type narrowing
واحدة من أقوى ميزات TypeScript هي قدرته على تضييق الأنواع بناءً على الشروط.
مثال:
type ResponseState =
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
function ResultView(state: ResponseState) {
if (state.status === "loading") {
return <p>جارٍ التحميل...</p>;
}
if (state.status === "error") {
return <p>{state.message}</p>;
}
return (
<ul>
{state.data.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
هذا النمط رائع جدًا. كل حالة واضحة، ولا تحتاج إلى افتراضات غير دقيقة. وهو مناسب بشكل خاص لواجهات تعتمد على ثلاث حالات أو أكثر: تحميل، نجاح، خطأ، فارغ، إعادة محاولة، إلخ.
العمل مع Context API
في تطبيقات React متوسطة وكبيرة، Context API شائع جدًا. ومع TypeScript، يصبح أكثر وضوحًا وأمانًا.
import { createContext, useContext, useState, ReactNode } from "react";
type Theme = "light" | "dark";
type ThemeContextType = {
theme: Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
type ThemeProviderProps = {
children: ReactNode;
};
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
هنا نستخدم undefined في createContext حتى نُجبر أنفسنا على التحقق من وجود السياق قبل استخدامه. هذا أفضل من افتراض أنه موجود دائمًا. وفي الواقع، هذا النمط يوفر أخطاء واضحة جدًا بدل أخطاء مبهمة عند التشغيل.
كتابة custom hooks بوضوح
custom hooks من أجمل ما في React الحديث. ومع TypeScript تصبح قابلة لإعادة الاستخدام بشكل أكبر وأكثر ثقة.
مثال hook يدير fetch:
import { useEffect, useState } from "react";
type UseFetchState<T> = {
data: T | null;
loading: boolean;
error: string | null;
};
function useFetch<T>(url: string): UseFetchState<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error("Failed to fetch");
}
const result: T = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
ثم استعماله:
type Post = {
id: number;
title: string;
};
function PostsPage() {
const { data, loading, error } = useFetch<Post[]>("/api/posts");
if (loading) return <p>جارٍ التحميل...</p>;
if (error) return <p>{error}</p>;
return (
<div>
{data?.map((post) => (
<h3 key={post.id}>{post.title}</h3>
))}
</div>
);
}
هذا النوع من الـ hooks مفيد جدًا، لكنه يتطلب discipline قليلًا في تصميم generic types. وعندما تتقنه، ستشعر أن الكود أصبح أكثر احترافية ومرونة في نفس الوقت.
generics في React: متى تستخدمها؟
Generics من أكثر الميزات التي تُظهر قوة TypeScript فعلًا. وهي مفيدة عندما تريد كتابة كود يعمل مع أنواع مختلفة دون أن تفقد سلامة الأنواع.
مثال قائمة عامة:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return <div>{items.map(renderItem)}</div>;
}
الاستخدام:
type User = {
id: number;
name: string;
};
<List
items={[
{ id: 1, name: "Ahmed" },
{ id: 2, name: "Sara" },
]}
renderItem={(user) => <p key={user.id}>{user.name}</p>}
/>
هذا النمط قوي جدًا في المكونات العامة والمكتبات الداخلية. لكنه ليس مطلوبًا في كل مكان. لا تستخدم generics لمجرد الإعجاب بها. استخدمها عندما تحل مشكلة فعلية.
أفضل الممارسات عند استخدام TypeScript مع React
هناك بعض العادات التي تريحك جدًا على المدى الطويل:
أولًا، لا تُفارق typed props في المكونات العامة. المكون الذي سيُستخدم كثيرًا يجب أن يكون واضح النوع من البداية.
ثانيًا، لا تعتمد على any إلا عند الضرورة القصوى. any قد يبدو حلاً سريعًا، لكنه في الحقيقة يعطّل أهم فائدة في TypeScript. كلما قللت استخدامه، زادت الفائدة.
ثالثًا، استخدم unknown عندما لا تعرف النوع فعلًا. unknown أكثر أمانًا من any لأنه يجبرك على التحقق قبل الاستخدام.
function handleData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase());
}
}
رابعًا، اجعل types قريبة من المكوّنات التي تستخدمها. لا تكدّس كل الأنواع في ملف واحد ضخم ما لم يكن ذلك منطقيًا. التنظيم الجيد يسهّل القراءة.
خامسًا، لا تبالغ في التعقيد. TypeScript ليس اختبارًا للذكاء. الهدف هو الوضوح، وليس كتابة أكثر أنواع ممكنة تعقيدًا. إذا كان النوع بسيطًا وواضحًا، فغالبًا هذا أفضل.
سادسًا، استعمل التسمية الجيدة. النوع الجيد مع اسم سيء يبقى مزعجًا. اكتب أسماء مفهومة مثل UserCardProps و AuthState و ApiResponse.
أخطاء شائعة يقع فيها المطورون
حتى مع TypeScript، هناك أخطاء متكررة يجب الانتباه لها.
1) الإفراط في any
عندما تستخدم any كثيرًا، فأنت تطلب من TypeScript ألا يراقبك. وهذا يهزم الغرض الأساسي من الأداة.
2) تجاهل null
قيمة null أو undefined قد لا تسبب مشكلة في كل مرة، لكنها من أكثر المصادر الشائعة للأخطاء.
3) كتابة types غير متزامنة مع الواقع
أحيانًا يعرّف المطور type جميلًا جدًا، لكن الـ API يرجع شيئًا مختلفًا. هنا لا بد من مراجعة العلاقة بين العقد والواقع.
4) عدم استخدام union types في الحالات المحددة
بدل كتابة:
type Status = string;
الأفضل غالبًا:
type Status = "idle" | "loading" | "success" | "error";
هذا الاختيار البسيط يقلل الأخطاء بشكل كبير.
5) تكرار الأنواع بشكل مفرط
إذا كتبت نفس shape في عدة أماكن، فقد يكون الوقت قد حان لاستخراج نوع مشترك.
تنظيم المشروع بطريقة صحية
في مشروع React + TypeScript جيد، سترى عادة أنواعًا موزعة بوعي:
الأنواع الخاصة بمكوّن معين تبقى بجانبه.
الأنواع المشتركة تُوضع في ملفات مخصصة.
أنواع الـ API تُفصل عن أنواع الـ UI إذا اختلف شكلها.
الـ utility types تُستخدم عندما تكون مناسبة فقط.
مثال هيكل بسيط:
src/
components/
Button/
Button.tsx
Button.types.ts
hooks/
useFetch.ts
types/
user.ts
api.ts
pages/
UsersPage.tsx
هذا الترتيب ليس قانونًا ثابتًا، لكنه يساعد على النمو الصحي للمشروع.
تحويل مشروع JavaScript إلى TypeScript تدريجيًا
كثير من الفرق تمتلك مشروع React قديمًا مكتوبًا بـ JavaScript، والانتقال إلى TypeScript قد يبدو مخيفًا في البداية. لكن الحقيقة أنه يمكن القيام به تدريجيًا وبهدوء.
الخطوات العملية غالبًا تكون كالتالي:
ابدأ بتثبيت TypeScript وإعداد الملفات الأساسية. ثم حوّل الملفات الأكثر استقرارًا والأكثر فائدة من ناحية typing، مثل المكونات العامة والـ utilities. بعد ذلك انتقل إلى أجزاء البيانات والنماذج. لا تبدأ بأكثر الملفات تعقيدًا إذا لم تكن مضطرًا. اجعل الانتقال منطقيًا ومقسّمًا.
يمكن أيضًا استخدام إعدادات أقل صرامة في البداية، ثم تشديدها مع الوقت. بهذه الطريقة لا يتحول الانتقال إلى عائق إنتاجي. الهدف هو تحسين المشروع، لا تعطيله.
TypeScript و React في العمل اليومي
عندما تعمل يوميًا على مشروع كبير، ستلاحظ أن TypeScript يغيّر أسلوبك قليلًا. ستصبح أكثر هدوءًا عند refactoring. ستفهم مكوناتك أكثر. وستقل المفاجآت الصغيرة التي تضيع وقتك.
على سبيل المثال، عندما تغيّر شكل object في مكان ما، سيذكرك TypeScript بكل الأماكن المتأثرة. هذا أمر بالغ القيمة في المشاريع الطويلة العمر. بدل أن تكتشف المشكلة بعد وصولها للمستخدم، تراها مبكرًا في المحرر.
وفي الفرق، يصبح الكود أكثر قابلية للمراجعة. الـ PRs تصبح أوضح. والاختلافات بين النسخ تصبح أسهل في الفهم. هذا ينعكس على جودة العمل كله، وليس فقط على “اختفاء بعض الأخطاء”.
هل TypeScript يعقد React؟
هذا سؤال مهم جدًا، والجواب الصادق هو: في البداية، نعم قليلًا. ثم لا، غالبًا يصبح أسهل.
المرحلة الأولى قد تشعر فيها أن الكتابة أبطأ لأنك تفكر في الأنواع. لكن بعد فترة قصيرة، يبدأ المحرر في مساعدتك، وتبدأ أنت في فهم أشكال البيانات بصورة أفضل، ثم يتحول TypeScript من عبء إلى مساعد حقيقي.
في المشاريع الصغيرة جدًا، قد لا يكون الالتزام الكامل به ضروريًا من اليوم الأول. لكن كلما كبر المشروع، كلما أصبحت فائدته أكبر. وهذه ليست مبالغة، بل تجربة عملية يلاحظها أغلب الفرق.
مثال تطبيقي كامل: صفحة ملفات المستخدمين
لنأخذ مثالًا أكثر واقعية يجمع عدة مفاهيم.
import { useEffect, useState } from "react";
type FileItem = {
id: number;
name: string;
size: number;
type: "image" | "pdf" | "doc";
};
type FilesState =
| { status: "loading" }
| { status: "error"; message: string }
| { status: "success"; files: FileItem[] };
async function fetchFiles(): Promise<FileItem[]> {
const response = await fetch("/api/files");
if (!response.ok) {
throw new Error("Unable to load files");
}
return response.json();
}
function FilesPage() {
const [state, setState] = useState<FilesState>({ status: "loading" });
useEffect(() => {
async function loadFiles() {
try {
const files = await fetchFiles();
setState({ status: "success", files });
} catch (err) {
setState({
status: "error",
message: err instanceof Error ? err.message : "Unknown error",
});
}
}
loadFiles();
}, []);
if (state.status === "loading") {
return <p>جارٍ تحميل الملفات...</p>;
}
if (state.status === "error") {
return <p>{state.message}</p>;
}
return (
<div>
<h2>الملفات</h2>
<ul>
{state.files.map((file) => (
<li key={file.id}>
{file.name} — {file.size} KB — {file.type}
</li>
))}
</ul>
</div>
);
}
export default FilesPage;
هذا المثال فيه typing للبيانات، typing للحالات، typing للـ API، وتضييق للأنواع عند العرض. والنتيجة واجهة أكثر وضوحًا وأقل عرضة للأخطاء.
رسالة أخيرة للمطور الذي يبدأ اليوم
قد يبدو TypeScript في البداية وكأنه طبقة إضافية فوق React، لكن الحقيقة أنه مع الوقت يتحول إلى جزء من أسلوبك في التفكير. أنت لا تكتب الكود فقط، بل تكتب معه خريطة للبيانات والعلاقات والحالات الممكنة. وهذه الخريطة هي ما يجعل التطبيق قابلًا للفهم بعد شهر، وقابلًا للتوسعة بعد سنة، وقابلًا للإصلاح بعد أول مشكلة حقيقية.
الجمال في TypeScript مع React ليس أنه يمنع الأخطاء فقط، بل أنه يجبرك على أن تكون أكثر وضوحًا مع نفسك أولًا، ثم مع فريقك، ثم مع المستخدم النهائي. وهذا الوضوح هو ما يميز المشاريع المتينة عن المشاريع المتعبة.
إذا بدأت اليوم بمكون صغير، أو hook واحد، أو صفحة واحدة، فأنت لا تبني مجرد typing للكود، بل تبني عادة هندسية أفضل. ومع الوقت، ستلاحظ أن كثيرًا من اللحظات التي كانت تربكك في JavaScript أصبحت أكثر هدوءًا ووضوحًا. وهذا، بصراحة، من أكثر الأشياء التي تجعل العمل ممتعًا من جديد.
خاتمة
استخدام TypeScript مع React Applications ليس مجرد خيار تقني، بل هو استثمار في جودة المشروع واستقراره وسهولة فهمه. من الـ props إلى الـ state، ومن الـ events إلى الـ hooks، ومن الـ API إلى الـ context، يضيف TypeScript طبقة من الوضوح تجعل كل جزء من التطبيق أكثر ثقة وأقل عشوائية. وعندما يُستخدم بشكل متوازن، فإنه لا يعقّد التطوير، بل ينضجه.