كل المقالات
TypeScriptنُشر 9 دقائق قراءة

أفضل ممارسات TypeScript في مشاريع React

قواعد عملية لكتابة TypeScript أنظف في React: strict mode، أنواع الـ props، discriminated unions، generics، أنواع الـ API، وsatisfies.

  • TypeScript
  • React
  • Best Practices
  • Frontend

أغلب مشاريع React بتوصل لـ TypeScript في مرحلة معينة، وبعدها بفترة بيبدأ الكود يتحول لـ any وas و@ts-ignore. المشكلة مش في TypeScript، المشكلة إنه اتضاف كطبقة فوق كود مش مصمم للأنواع.

القواعد اللي جاية مش قوانين مقدسة، لكنها اشتغلت معايا وبتقلل الاحتكاك اليومي.

1. فعّل strict mode من أول يوم

ملف tsconfig.json لازم يبدأ بـ strict: true. بعد كده فعّل noUncheckedIndexedAccess لو تقدر، لأنه بيجبرك تتعامل مع احتمال إن العنصر مش موجود. الألم في البداية أقل بكتير من الألم بعد ست شهور.

json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true
  }
}

2. عرّف أنواع الـ props بوضوح

استخدم type أو interface للـ props، وابعد عن React.FC إلا لو محتاج تفاصيل معينة. النوع الصريح بيوضح shape المكون وبيخلي الـ error messages أوضح.

tsx
type ProjectCardProps = {
  title: string;
  description: string;
  stack: readonly string[];
  href: string;
  featured?: boolean;
};

export function ProjectCard({
  title,
  description,
  stack,
  href,
  featured = false,
}: ProjectCardProps) {
  return (
    <article data-featured={featured}>
      <h3>{title}</h3>
      <p>{description}</p>
      <ul>
        {stack.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
      <a href={href}>View project</a>
    </article>
  );
}

لو المكون بيمرر عناصر HTML، استخدم ComponentProps بدل ما تعيد تعريف className وchildren وonClick بنفسك:

tsx
type ButtonProps = React.ComponentProps<"button"> & {
  variant?: "primary" | "secondary";
};

export function Button({ variant = "primary", ...props }: ButtonProps) {
  return <button data-variant={variant} {...props} />;
}

3. بلاش any — استخدم unknown لو مش عارف

any بيوقف التحقق ويكسر الغرض من TypeScript. لو النوع مش معروف فعلًا، استخدم unknown وبعدين ضيّق النوع بـ type guard. ده بيجبرك تتعامل مع الحالات اللي ممكن تفشل.

ts
function isProject(value: unknown): value is Project {
  return (
    typeof value === "object" &&
    value !== null &&
    "slug" in value &&
    typeof (value as { slug: unknown }).slug === "string"
  );
}

4. استخدم discriminated unions للحالات

أي حالة ليها variants — loading وsuccess وerror — لازم تكون union مفصولة، مش كائن فيه flags اختيارية. ده بيمنع حالات مستحيلة زي isLoading && data.

ts
type State<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

function render(state: State<Project[]>) {
  switch (state.status) {
    case "idle":
      return null;
    case "loading":
      return <Spinner />;
    case "success":
      return <ProjectList projects={state.data} />;
    case "error":
      return <ErrorMessage message={state.message} />;
  }
}

5. Generics لما العلاقة بين الأنواع مهمة

Generics مفيدة لما نوع المخرج مرتبط بنوع المدخل. متستخدمهاش لمجرد إنها “متقدمة”. المثال الكلاسيكي هو دالة fetch أو hook بيرجع نفس نوع البيانات اللي بيدخله.

ts
async function getJson<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error("Request failed: " + response.status);
  }
  return (await response.json()) as T;
}

const projects = await getJson<Project[]>("/api/projects");

ملاحظة: as T هنا مش تحقق حقيقي من البيانات. في الحدود الخارجية، استخدم schema validation زي Zod أو تحقق يدوي. TypeScript بيوصف الشكل، مش بيتحقق منه وقت التشغيل.

6. اعتبر الـ API حدود غير موثوقة

أي بيانات جاية من API أو localStorage أو form لازم تتحقق. النوع اللي بتكتبه مش ضمان إن السيرفر هيبعت نفس الشكل. عرّف نوع الاستجابة، وتحقق منه، وتعامل مع الأخطاء.

ts
type ApiProject = {
  slug: string;
  name: string;
  stack: string[];
};

function parseProject(value: unknown): ApiProject | null {
  if (typeof value !== "object" || value === null) return null;
  const candidate = value as Record<string, unknown>;
  if (typeof candidate.slug !== "string") return null;
  if (typeof candidate.name !== "string") return null;
  if (!Array.isArray(candidate.stack)) return null;
  if (!candidate.stack.every((item) => typeof item === "string")) return null;
  return {
    slug: candidate.slug,
    name: candidate.name,
    stack: candidate.stack,
  };
}

7. استخدم satisfies و as const بذكاء

satisfies بيتحقق إن القيمة مطابقة لنوع معين من غير ما يوسّع نوعها. ده مفيد في كائنات الإعدادات والخرائط. as const بيثبت القيم الحرفية ويخلي المصفوفات readonly.

ts
const nav = [
  { label: "Work", href: "/projects" },
  { label: "About", href: "/about" },
] as const satisfies ReadonlyArray<{ label: string; href: string }>;

type NavHref = (typeof nav)[number]["href"];

8. خلي أنواع الفورم قريبة من الفورم

لو الفورم صغير، نوع واحد للقيم وحالة للتحقق كفاية. لو الفورم كبير، استخدم schema validation وderive النوع منه. الأهم إن الأخطاء تكون مربوطة بالحقول، وإن الحالة تكون واضحة.

tsx
type ContactValues = {
  name: string;
  email: string;
  message: string;
};

type ContactErrors = Partial<Record<keyof ContactValues, string>>;

function validate(values: ContactValues): ContactErrors {
  const errors: ContactErrors = {};
  if (!values.name.trim()) errors.name = "Name is required";
  if (!values.email.includes("@")) errors.email = "Enter a valid email";
  if (values.message.trim().length < 10) {
    errors.message = "Tell me a little more";
  }
  return errors;
}

خلاصة

TypeScript مش هدف في حد ذاته. الهدف إن الكود يوضح نيته، وإن الأخطاء تظهر بدري، وإن إعادة الهيكلة تبقى آمنة. ابدأ بـ strict، استخدم أنواع واضحة، اضغط على الحدود الخارجية، وابعد عن any. الباقي تفاصيل بتتحسن مع الوقت.

بتبني حاجة شبه دي؟

دايمًا مبسوط أتكلم عن المعمارية أو المقارنات أو مشروع واقف فيه.

ابدأ الكلام