مقالات › TypeScript

آموزش TypeScript برای برنامه‌نویسان React؛ تایپ Props، State، هوک‌ها و داده‌ی API

راهنمای عملی TypeScript در React: تایپ کردن Props و children، State، رویدادها، Ref، هوک‌های سفارشی، Context، کامپوننت جنریک، پاسخ API و Utility Typeهای پرکاربرد.

TypeScript اشتباهات را قبل از اینکه کاربرانتان ببینند پیدا می‌کند: یک Prop با املای اشتباه، فیلدی که در پاسخ API نیست یا تابعی که با آرگومان اشتباه صدا زده شده. در پروژه‌های React ویرایشگر شما را هم به مستندات تبدیل می‌کند؛ تکمیل خودکار دقیقاً نشان می‌دهد هر کامپوننت چه Propهایی می‌پذیرد. این راهنما همان TypeScriptی را پوشش می‌دهد که در کار روزانه با React واقعاً استفاده می‌کنید.

راه‌اندازی

npm create vite@latest my-app -- --template react-ts

تنظیمات کلیدی tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "jsx": "react-jsx",
    "moduleResolution": "bundler"
  }
}

strict را روشن نگه دارید. خاموش کردنش بیشتر ارزش TypeScript را از بین می‌برد.

تایپ کردن Props

type ButtonProps = {
  label: string;
  variant?: "primary" | "ghost";        // اختیاری و محدود به دو مقدار
  disabled?: boolean;
  onClick: () => void;
};

export function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
  return (
    <button className={`btn btn--${variant}`} disabled={disabled} onClick={onClick}>
      {label}
    </button>
  );
}

<Button label="Save" onClick={save} />
<Button label="Save" variant="danger" onClick={save} />  // ❌ خطای نوع

type و interface هر دو برای Props کار می‌کنند؛ یک سبک را انتخاب و یکدست استفاده کنید. از React.FC پرهیز کنید؛ تایپ مستقیم Props ساده‌تر است و با جنریک‌ها بهتر کار می‌کند.

children و ویژگی‌های بومی المان

type CardProps = {
  title: string;
  children: React.ReactNode;
};

// گسترش Propsهای بومی المان برای پذیرفتن className، id، aria-* و...
type InputProps = React.ComponentProps<"input"> & {
  label: string;
  error?: string;
};

function TextField({ label, error, ...inputProps }: InputProps) {
  return (
    <label>
      {label}
      <input {...inputProps} aria-invalid={Boolean(error)} />
      {error && <span role="alert">{error}</span>}
    </label>
  );
}

تایپ کردن State

TypeScript نوع Stateهای ساده را استنتاج می‌کند:

const [count, setCount] = useState(0);            // number
const [query, setQuery] = useState("");           // string

وقتی مقدار اولیه همه‌ی حالت‌ها را توصیف نمی‌کند، نوع را مشخص کنید:

type User = { id: number; name: string; email: string };

const [user, setUser] = useState<User | null>(null);
const [tags, setTags] = useState<string[]>([]);

Discriminated Union برای State ناهمگام

به‌جای سه Boolean مستقل که ممکن است با هم تناقض داشته باشند، حالت‌ها را صریح مدل کنید:

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

function render(state: RequestState<User[]>) {
  switch (state.status) {
    case "loading": return <Spinner />;
    case "error":   return <p>{state.error}</p>;      // TS می‌داند اینجا error وجود دارد
    case "success": return <UserList users={state.data} />;
    default:        return null;
  }
}

حالت‌های غیرممکن مثل «در حال بارگذاری و خطا» اصلاً قابل نمایش نیستند.

تایپ کردن رویدادها

function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => onSearch(e.target.value);
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };
  const handleKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Escape") onSearch("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKey} />
    </form>
  );
}

نکته: ماوس را روی Prop onChange در ویرایشگر نگه دارید تا نوع دقیق رویداد را ببینید.

تایپ کردن Ref

const inputRef = useRef<HTMLInputElement>(null);       // Ref به DOM
const timerRef = useRef<number | undefined>(undefined); // مقدار تغییرپذیر

useEffect(() => {
  inputRef.current?.focus();   // current ممکن است null باشد
}, []);

تایپ کردن هوک سفارشی

function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? (JSON.parse(stored) as T) : initial;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;   // Tuple، نه (T | Dispatch)[]
}

const [theme, setTheme] = useLocalStorage<"dark" | "light">("theme", "dark");

as const باعث می‌شود Tuple برگشتی دقیق تایپ شود.

تایپ کردن Context

type AuthContextValue = {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
};

const AuthContext = createContext<AuthContextValue | null>(null);

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
  return ctx;   // AuthContextValue — امکان null حذف شده است
}

کامپوننت‌های جنریک

جنریک‌ها اجازه می‌دهند یک کامپوننت با انواع داده‌ی مختلف کار کند و ایمنی نوع حفظ شود:

type ListProps<T> = {
  items: T[];
  getKey: (item: T) => string | number;
  renderItem: (item: T) => React.ReactNode;
};

function List<T>({ items, getKey, renderItem }: ListProps<T>) {
  return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}

<List
  items={articles}
  getKey={(a) => a.id}
  renderItem={(a) => a.title}   // a به‌صورت Article استنتاج می‌شود
/>

تایپ کردن داده‌ی API

نوع‌های TypeScript در زمان اجرا وجود ندارند؛ پس سرور همچنان می‌تواند داده‌ی غیرمنتظره بفرستد. برای پاسخ‌ها نوع تعریف کنید و برای داده‌های حیاتی در زمان اجرا هم اعتبارسنجی کنید:

type ArticleSummary = {
  id: number;
  slug: string;
  title: string;
  publishedAt: string;   // تاریخ ISO به‌صورت رشته از JSON
};

async function getArticles(): Promise<ArticleSummary[]> {
  const res = await fetch("/api/articles");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<ArticleSummary[]>;
}

با یک کتابخانه‌ی Schema مثل Zod، یک تعریف هم اعتبارسنجی زمان اجرا و هم نوع TypeScript را می‌دهد:

import { z } from "zod";

const Article = z.object({ id: z.number(), slug: z.string(), title: z.string() });
type Article = z.infer<typeof Article>;

const data = Article.array().parse(await res.json());

می‌توانید نوع‌ها را از سند OpenAPI در ASP.NET Core هم تولید کنید (مثلاً با openapi-typescript) تا فرانت‌اند و بک‌اند همگام بمانند.

Utility Typeهای پرکاربرد

ابزار کاربرد مثال
Partial<T> همه‌ی Propertyها اختیاری Payload به‌روزرسانی
Pick<T, K> انتخاب چند Property Pick<User, "id" \| "name">
Omit<T, K> حذف Propertyها Omit<User, "password">
Record<K, V> شیء نگاشتی Record<Lang, string>
ReturnType<F> نوع خروجی یک تابع استخراج نوع از توابع کمکی
NonNullable<T> حذف null/undefined بعد از یک بررسی
type Lang = "fa" | "en";
const labels: Record<Lang, string> = { fa: "فارسی", en: "English" };

اشتباهات رایج

  1. any همه‌جا — بررسی نوع را خاموش می‌کند. به‌جای آن از unknown استفاده و آن را محدود (Narrow) کنید.
  2. Non-null Assertion (!) برای ساکت کردن خطا — حالت null را مدیریت کنید.
  3. Cast کورکورانه‌ی پاسخ API برای داده‌ای که واقعاً ممکن است متفاوت باشد — اعتبارسنجی کنید.
  4. مهندسی بیش از حد نوع‌ها — اگر خواندن یک نوع از خود کد سخت‌تر است، ساده‌اش کنید.

جمع‌بندی

Props را صریح تایپ کنید، بگذارید TypeScript نوع Stateهای ساده را استنتاج کند، State ناهمگام را با Discriminated Union مدل کنید، از نوع‌های رویداد و المان React استفاده کنید، Tupleهای هوک را با as const برگردانید، دسترسی به Context را محافظت کنید، برای لیست‌ها و جدول‌های قابل استفاده‌ی مجدد کامپوننت جنریک بنویسید و داده‌ی API را تا اعتبارسنجی نشده غیرقابل اعتماد بدانید. با حالت strict، TypeScript به یک تور ایمنی تبدیل می‌شود که Refactor در React را بی‌دغدغه می‌کند.

پرسش‌های متداول

برای Props از type استفاده کنم یا interface؟

هر دو کار می‌کنند. خیلی از تیم‌های React به‌خاطر انعطاف با Unionها type و برخی برای گسترش‌پذیری interface را ترجیح می‌دهند. یکدست بودن مهم‌تر است.

آیا TypeScript برای پروژه‌های کوچک ارزش دارد؟

معمولاً بله؛ راه‌اندازی با Vite ساده است و فقط تکمیل خودکار به‌سرعت هزینه‌اش را جبران می‌کند.

آیا TypeScript برنامه را کند می‌کند؟

نه. نوع‌ها در زمان Build حذف می‌شوند و جاوااسکریپتی که اجرا می‌شود همان است.