در یکی از پروژه‌های React که تیم دیگری نوشته بود، با مورد عجیبی روبرو شدم: کدبیس با strict: true کامپایل می‌شد، هیچ خطای تایپی وجود نداشت، اما کاربران همچنان باگ گزارش می‌کردند. بعد از دو روز بررسی، فهمیدم مشکل در جایی است که کمترین کسی به آن نگاه می‌کند: تایپ Props کامپوننت‌ها ناقص بود. یک کامپوننت با children: React.ReactNode تعریف شده بود اما در واقع یک تابع callback خاص را از پدر انتظار داشت. ReactNode اجازه می‌داد هر چیزی پاس شود و کسی متوجه نشده بود. آن پروژه نگاه من به ترکیب TypeScript و React را برای همیشه تغییر داد: TS در React یک لایه‌ی محافظتی است، اما فقط اگر واقعاً محافظت کند.

React و TypeScript یک ترکیب طبیعی‌اند. React ذاتاً از توابع و props ساخته می‌شود و تایپ‌دهی به این ساختار، دقیقاً همان چیزی است که TS برای آن طراحی شده. اما تجربه‌ی من در ده‌ها پروژه نشان می‌دهد که کمتر از ۳۰٪ تیم‌ها از پتانسیل کامل این ترکیب استفاده می‌کنند. اگر در مسیر آموزش تایپ اسکریپت از صفر هستید و مقالات React از صفر و اینترفیس در تایپ اسکریپت را خوانده‌اید، این نوشته تمام نقاط تماس این دو ابزار را باز می‌کند.

چرا ترکیب TypeScript و React یک انتخاب معماری است؟

React یک کتابخانه‌ی مبتنی بر توابع است: هر کامپوننت، یک تابع است که Props می‌گیرد و JSX برمی‌گرداند. این ساختار، به‌طور طبیعی با TypeScript هم‌خوانی دارد. سه دلیل که این ترکیب را جدی می‌گیرم:

  • Props یک قرارداد ورودی هستند: هر کامپوننت React یک تابع است با یک ورودی مشخص. TypeScript می‌تواند این ورودی را به‌طور دقیق تایپ‌دهی کند و از پاس شدن مقادیر نادرست جلوگیری کند. مطالعه‌ی موازی در اینترفیس در تایپ اسکریپت.
  • State یک تایپ پویا دارد: useState در React می‌تواند هر تایپی را نگه دارد. TS این امکان را می‌دهد که تایپ State را دقیق مشخص کنید و از باگ‌های مربوط به مقادیر نامعتبر جلوگیری کنید.
  • Hookها یک قرارداد دارند: Custom Hookها الگوهای تکرارشونده‌ای هستند که با TS می‌توانند تایپ‌دهی دقیق داشته باشند. این یعنی در کل پروژه، یک الگوی واحد با تایپ‌چکینگ دقیق. مطالعه‌ی این لایه در هوک‌های React آمده است.

در یک پروژه‌ی داشبورد مدیریتی که تیم شش نفره داشت، بعد از مهاجرت از JS به TS، تعداد باگ‌های مربوط به Props نادرست در production به‌طور محسوس کاهش پیدا کرد. علت: کامپایلر TS دقیقاً می‌دانست چه چیزی به هر کامپوننت پاس می‌شود و اگر تایپی مطابقت نداشت، جلوی build را می‌گرفت. این یک صرفه‌جویی مستقیم در زمان دیباگ بود.

TypeScript در React، تفاوت بین یک کامپوننت که «کار می‌کند» و یک کامپوننت که «به‌درستی کار می‌کند» است — و این تفاوت، در پروژه‌های بزرگ، چند برابر می‌شود.

راه‌اندازی اولین پروژه React + TypeScript

ساخت پروژه‌ی React با TypeScript از سال‌ها پیش آسان شده. سه روش اصلی:

Vite (روش توصیه‌شده)

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

Vite یک الگوی آماده‌ی react-ts دارد که تمام تنظیمات را از ابتدا درست انجام می‌دهد. مزیت: build سریع، HMR بدون وقفه، و تنظیمات TS و Vite که از قبل هماهنگ شده‌اند.

Next.js

npx create-next-app@latest my-app --typescript

اگر پروژه‌ی SSR یا SSG می‌سازید، Next.js انتخاب استاندارد است و از TypeScript به‌طور بومی پشتیبانی می‌کند. مطالعه‌ی موازی در تایپ اسکریپت با Node.js.

افزودن TS به پروژه‌ی موجود

npm install --save-dev typescript @types/react @types/react-dom

سپس یک فایل tsconfig.json بسازید و فایل‌های React را با پسوند .tsx ذخیره کنید. الگوی استاندارد در tsconfig برای پروژه‌ی React را در تنظیمات tsconfig کامل توضیح داده‌ام.

نکته‌ی مهم: در tsconfig پروژه‌های React، باید "jsx": "react-jsx" فعال باشد. این تنظیم مشخص می‌کند که JSX چطور transpile شود. در نسخه‌های جدید React (۱۷+)، این تنظیم استاندارد است.

تایپ‌دهی به Props: پایه‌ای‌ترین لایه

ساده‌ترین و پرکاربردترین لایه‌ی تایپ‌دهی در React، Props است:

interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
}

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

سه نکته‌ی مهم در تایپ Props:

  1. استفاده از interface یا type: هر دو کار می‌کنند اما قاعده‌ی من در پروژه‌ها: از interface برای Props استفاده کنید چون در editor سریع‌تر و در تعارض نام‌ها راحت‌تر است. مطالعه‌ی مقایسه‌ی کامل در اینترفیس در تایپ اسکریپت.
  2. مقادیر پیش‌فرض با destructuring: به‌جای defaultProps (که در نسخه‌های جدید React deprecate شده)، از مقدار پیش‌فرض در destructuring استفاده کنید.
  3. تایپ‌های Literal برای حالت‌ها: به‌جای variant: string، از variant: "primary" | "secondary" | "danger" استفاده کنید. این یعنی اگر جایی به‌اشتباه "primery" بنویسید، کامپایلر در همان لحظه خطا می‌دهد.

یک الگوی تکمیلی که در پروژه‌ها زیاد استفاده می‌کنم:

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  label: string;
  variant?: "primary" | "secondary";
}

با این الگو، کامپوننت Button شما تمام ویژگی‌های بومی <button> (مثل type، form، aria-label) را به‌طور خودکار می‌پذیرد، بدون اینکه لازم باشد همه را تایپ کنید. مطالعه‌ی موازی این لایه در تایپ ها در تایپ اسکریپت.

Children و ReactNode: تفاوت‌هایی که همه اشتباه می‌گیرند

یکی از پرکاربردترین و در عین حال پر ابهام تایپ‌ها در React، children است:

interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div>{children}</div>
    </div>
  );
}

سه تایپ مختلف که همه برای children استفاده می‌شوند اما معنای متفاوتی دارند:

تایپمعنیکاربرد
React.ReactNodeهر چیزی که قابل رندر در JSX استحالت عمومی (اکثر موارد)
React.ReactElementفقط یک المان Reactوقتی مطمئنید فقط یک المان می‌آید
React.ReactElement[]آرایه‌ای از المان‌هاوقتی از لیست مطمئنید
JSX.Elementیک المان JSXمشابه ReactElement با تفاوت‌های ظریف
string | numberفقط متنوقتی children فقط متن است

الگوی من در پروژه‌ها: در ۹۰٪ موارد، React.ReactNode انتخاب درست است. اما یک اشتباه رایج: استفاده از ReactNode برای چیزی که در واقع callback است. مثلاً:

// اشتباه - ReactNode اجازه می‌دهد هر چیزی پاس شود
interface Props {
  render: React.ReactNode;
}

// درست - مشخص کردن تابع با امضای دقیق
interface Props {
  render: (item: Item) => React.ReactNode;
}

این تفاوت در پروژه‌ای به یک باگ بزرگ تبدیل شد: توسعه‌دهنده‌ای به‌جای پاس دادن یک تابع، یک عدد پاس داده بود و ReactNode آن را قبول کرده بود. در runtime، فراخوانی آن به‌عنوان تابع خطا داد. اگر تایپ درست بود، کامپایلر همان لحظه جلوی این خطا را می‌گرفت.

تایپ‌دهی به useState و State Management

useState در اکثر موارد تایپ را به‌طور خودکار استنتاج می‌کند، اما در بعضی سناریوها نیاز به تایپ صریح دارد:

// استنتاج خودکار - کافی است
const [count, setCount] = useState(0);
const [name, setName] = useState("Ali");

// نیاز به تایپ صریح - مقدار اولیه null یا undefined
interface User {
  id: number;
  name: string;
}

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

// نیاز به تایپ صریح - union complex
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");

سه نکته‌ی مهم:

  • تایپ Union برای State: اگر State یک حالت مشخص دارد (idle/loading/error)، از Union با Literal Types استفاده کنید. این یک الگوی استاندارد در React است و از پاس شدن مقادیر نامعتبر جلوگیری می‌کند.
  • null به‌عنوان مقدار اولیه: اگر مقدار اولیه null است، باید تایپ صریح useState<T | null>(null) بنویسید. بدون تایپ، TS فرض می‌کند State فقط null است.
  • تایپ‌دهی به Setter: در نسخه‌های جدید React، setState یک تایپ دقیق دارد. می‌توانید مقادیر را با تابع یا مستقیم پاس دهید و TS هر دو را می‌پذیرد. اما اگر State شما تابع است، باید از useState<() => void>(...) استفاده کنید نه useState(...).

در پروژه‌ای که یک سیستم مدیریت کاربران بود، استفاده‌ی گسترده از Union Types برای State، خطاهای مربوط به حالت‌های نامعتبر را به‌طور محسوس کاهش داد. قبل از این الگو، هر جا State روی یک مقدار اشتباه می‌رفت، در runtime باگ عجیبی رخ می‌داد. مطالعه‌ی موازی در هوک‌های React.

Event Handlerها و تایپ‌دهی درست آن‌ها

Event Handlerها یکی از پرکاربردترین نقاط تماس TS و React هستند. React یک سیستم رویداد مخصوص خودش دارد که در TS به‌صورت تایپ‌های مشخص نمایان می‌شود:

function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
  console.log(event.currentTarget.value);
}

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  console.log(event.target.value);
}

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault();
  // ...
}

پرکاربردترین Event Types در پروژه‌های React:

  • React.MouseEvent<T> — کلیک، hover، mouseEnter و غیره.
  • React.ChangeEvent<T> — تغییر مقدار input، select، textarea.
  • React.FormEvent<T> — رویدادهای فرم.
  • React.KeyboardEvent<T> — رویدادهای کیبورد.
  • React.FocusEvent<T> — رویدادهای focus و blur.
  • React.TouchEvent<T> — رویدادهای لمسی موبایل.
  • React.ClipboardEvent<T> — کپی/پیست.
  • React.DragEvent<T> — drag and drop.

نکته‌ی مهم در حرف T: این پارامتر تایپ بومی HTML آن المان است. مثلاً برای input، HTMLInputElement و برای button، HTMLButtonElement. این تایپ به شما امکان می‌دهد که به تمام ویژگی‌های بومی المان دسترسی داشته باشید:

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  const value: string = event.target.value;  // TS می‌داند value یک string است
}

function handleCheckbox(event: React.ChangeEvent<HTMLInputElement>) {
  const checked: boolean = event.target.checked;  // TS می‌داند checked یک boolean است
}

یک الگوی کاربردی که در پروژه‌ها زیاد استفاده می‌کنم:

type InputChangeHandler = (
  event: React.ChangeEvent<HTMLInputElement>
) => void;

function useFormInput(initialValue: string) {
  const [value, setValue] = useState(initialValue);

  const handleChange: InputChangeHandler = (event) => {
    setValue(event.target.value);
  };

  return { value, handleChange };
}

با این الگو، تعریف Event Handlerها به‌طور متمرکز انجام می‌شود و در جای مختلف، فقط از تایپ استفاده می‌کنید. مطالعه‌ی موازی در تایپ ها در تایپ اسکریپت.

Event Handlerها در React، دیوارِ محافظ بین کاربر و کد شما هستند؛ اگر تایپ آن‌ها ناقص باشد، این دیوار اولین جایی است که در runtime فرو می‌ریزد.

useRef و useEffect در TypeScript

دو Hook پرکاربرد که تایپ‌دهی درستی می‌خواهند:

useRef

const inputRef = useRef<HTMLInputElement>(null);
const countRef = useRef<number>(0);

useEffect(() => {
  inputRef.current?.focus();
  countRef.current = 5;
}, []);

<input ref={inputRef} />

نکته: useRef<HTMLInputElement>(null) تایپ را دقیقاً HTMLInputElement | null می‌کند. یعنی وقتی به current دسترسی می‌گیرید، TS می‌داند که ممکن است null باشد و شما را مجبور به بررسی می‌کند.

useEffect

useEffect(() => {
  const timer = setInterval(() => {
    // ...
  }, 1000);

  return () => clearInterval(timer);
}, [user.id]);

useEffect در اکثر موارد نیاز به تایپ صریح ندارد اما باید مراقب دو نکته باشید:

  • تایپ cleanup function: تابع بازگشتی باید یا void باشد یا یک تابع cleanup برگرداند. TS این را بررسی می‌کند اما در بعضی موارد، اشتباهات ظریف از دستش در می‌رود.
  • تایپ dependency array: React نمی‌تواند نوع ورودی‌های dependency array را بررسی کند. اگر وابستگی‌ای اشتباه بدهید، TS آن را نمی‌گیرد. برای این کار باید به الگوهای طراحی مبتنی بر تمرین اعتماد کنید.

Custom Hookها با TypeScript

Custom Hookها یکی از قوی‌ترین الگوهای React هستند و با TS تبدیل به یک ابزار حرفه‌ای می‌شوند:

function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    const item = window.localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });

  const setValue = (value: T) => {
    setStoredValue(value);
    window.localStorage.setItem(key, JSON.stringify(value));
  };

  return [storedValue, setValue];
}

// استفاده
const [user, setUser] = useLocalStorage<User>("user", { id: 0, name: "" });
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");

این الگو در پروژه‌های واقعی بسیار مفید است. همان Custom Hook، هم برای User و هم برای تنظیمات تم، کار می‌کند با تایپ‌چکینگ دقیق. مطالعه‌ی بیشتر در جنریک در تایپ اسکریپت.

یک Custom Hook دیگر که در پروژه‌ها زیاد استفاده می‌کنم:

function useFetch<T>(url: string): {
  data: T | null;
  loading: boolean;
  error: Error | null;
} {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then((json: T) => {
        setData(json);
        setLoading(false);
      })
      .catch((err: Error) => {
        setError(err);
        setLoading(false);
      });
  }, [url]);

  return { data, loading, error };
}

// استفاده
const { data: users } = useFetch<User[]>("/api/users");

با این الگو، هر جا از API استفاده کنید، TS دقیقاً می‌داند data چه تایپی دارد. مطالعه‌ی موازی در هوک‌های React.

Context API و تایپ‌دهی امن

Context API یکی از جاهایی است که بدون TS، باگ‌های پنهان زیادی ایجاد می‌کند. بدون تایپ‌دهی، هر کسی می‌تواند مقدار Context را دستکاری کند. با TS، می‌توانید یک قرارداد محکم بسازید:

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

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

function useAuth(): AuthContextValue {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = async (email: string, password: string) => {
    // منطق ورود
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

نکته‌ی مهم: الگوی createContext<T | null>(null) به‌همراه یک Custom Hook که خطا می‌دهد اگر context null باشد، یکی از استانداردهای React + TypeScript است. این الگو تضمین می‌کند که هیچ‌وقت Context را از بیرون از Provider استفاده نکنید.

الگوی خوب: به‌جای export کردن خود Context، فقط Provider و Custom Hook را export کنید. این کار، استفاده‌ی نادرست از Context را دشوارتر می‌کند:

// در فایل AuthContext.tsx
export { AuthProvider, useAuth };
// AuthContext را export نمی‌کنیم

Generics در کامپوننت‌های React

Generics در React برای ساخت کامپوننت‌های قابل استفاده‌ی مجدد اساسی است. یکی از پرکاربردترین موارد، Table یا List است:

interface TableProps<T> {
  data: T[];
  columns: Array<{
    key: keyof T;
    header: string;
    render?: (value: T[keyof T], row: T) => React.ReactNode;
  }>;
  keyExtractor: (item: T) => string | number;
}

function Table<T>({ data, columns, keyExtractor }: TableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map(col => (
            <th key={String(col.key)}>{col.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map(item => (
          <tr key={keyExtractor(item)}>
            {columns.map(col => (
              <td key={String(col.key)}>
                {col.render ? col.render(item[col.key], item) : String(item[col.key])}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

استفاده:

const users: User[] = [...];

<Table
  data={users}
  columns={[
    { key: "id", header: "شناسه" },
    { key: "name", header: "نام" },
    { key: "email", header: "ایمیل", render: (v) => <a href={`mailto:${v}`}>{v}</a> }
  ]}
  keyExtractor={(u) => u.id}
/>

مزیت: اگر در columns یک key اشتباه بنویسید (مثلاً "fullname" به‌جای "name")، کامپایلر همان لحظه خطا می‌دهد. این یک لایه‌ی محافظت قوی است که در پروژه‌های جدولی بزرگ، بسیار ارزشمند است. مطالعه‌ی بیشتر در جنریک در تایپ اسکریپت.

الگوهای واقعی در پروژه‌های بزرگ

سه الگویی که در پروژه‌های خودم بیشترین استفاده را داشته‌اند:

  1. Discriminated Unions برای Props کامپوننت: در یک کامپوننت که می‌تواند حالت‌های مختلف داشته باشد (مثلاً Button که می‌تواند primary یا icon-only باشد)، از Union با Discriminator استفاده می‌کنیم. نتیجه: TS جلوگیری می‌کند از ترکیب‌های نامعتبر Props:
type ButtonProps =
  | { variant: "primary"; label: string; icon?: never }
  | { variant: "icon"; icon: React.ReactNode; label?: never };

function Button(props: ButtonProps) {
  if (props.variant === "primary") {
    return <button>{props.label}</button>;
  }
  return <button>{props.icon}</button>;
}
  1. Compound Components با تایپ‌دهی دقیق: در ساخت کامپوننت‌هایی مثل Card یا Tabs که چند بخش دارند، از Context داخلی استفاده می‌کنیم. تایپ‌دهی دقیق این الگو با TS، خیلی از باگ‌های مربوط به ساختار را حذف می‌کند.
  2. ForwardRef با Generics: در نسخه‌های قبل از React 19، برای پاس دادن ref به‌عنوان یک prop، از forwardRef استفاده می‌کردیم. این الگو با Generics به‌طور طبیعی کار می‌کند:
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
  <input ref={ref} {...props} />
));

اشتباهاتی که در پروژه‌ها دیدم

  • استفاده از any برای Props: شایع‌ترین اشتباه. اگر Props را با any تایپ کنید، کل کامپوننت از تایپ‌چکینگ خارج می‌شود. راه‌حل: همیشه Props را با interface تایپ کنید.
  • نادیده گرفتن Union Types در State: استفاده از useState<string> برای State که در واقع چند مقدار خاص دارد. راه‌حل: از Literal Types با Union استفاده کنید.
  • تایپ‌دهی ضعیف به Children: استفاده از ReactNode برای چیزی که در واقع callback است یا آرایه‌ای از المان‌های خاص. راه‌حل: با دقت تایپ دقیق‌تری انتخاب کنید.
  • نادیده گرفتن Event Types: استفاده از any برای Event Handlerها یا تعریف دستی تایپ. راه‌حل: از React.ChangeEvent و خانواده‌اش استفاده کنید.
  • useRef بدون تایپ: اگر useRef(null) بنویسید، TS فرض می‌کند current همیشه null است. راه‌حل: از useRef<T>(null) استفاده کنید.
  • نادیده گرفتن null در Context: تعریف Context با تایپ T به‌جای T | null و پاس دادن null به‌عنوان مقدار اولیه. راه‌حل: از الگوی createContext با null و Custom Hook محافظ استفاده کنید.
  • کاهش استفاده از Generics: در پروژه‌ها دیده‌ام که برای هر کامپوننت جدولی، یک نسخه‌ی جداگانه نوشته شده بود. با Generics، یک کامپوننت می‌تواند تمام موارد را پوشش دهد.
  • نادیده گرفتن React.Fragment: در بعضی موارد، برای پاس دادن children بدون div اضافه، از Fragment استفاده می‌کنیم. تایپ React.ReactNode این را پوشش می‌دهد اما نبود آن باعث کد اضافه می‌شود.
  • الگوهای ناسازگار در تیم: در بعضی تیم‌ها، هر توسعه‌دهنده یک سبک متفاوت برای تایپ‌دهی Props دارد. راه‌حل: تعریف یک راهنمای تیمی برای الگوهای TS در React. مطالعه‌ی موازی در خطاهای رایج تایپ اسکریپت.
  • نادیده گرفتن noUncheckedIndexedAccess: در tsconfig، این تنظیم باعث می‌شود که دسترسی به آرایه همیشه ممکن‌الخطا دیده شود. در پروژه‌های React که با آرایه‌های زیاد کار می‌کنید، این تنظیم یک لایه‌ی محافظت اضافه می‌کند. مطالعه‌ی بیشتر در تنظیمات tsconfig.

لایه‌ای پایین‌تر از کامپوننت‌ها

اینجا وارد لایه‌ای می‌شوم که در پروژه‌های معمولی به آن نگاه نمی‌شود اما برای مهندسان پلتفرم و توسعه‌دهنده‌های ارشد اهمیت دارد. آنچه React و TypeScript با کامپوننت‌های شما می‌کنند، در پنج مفهوم خلاصه می‌شود:

  1. JSX Transform و Type Erasure: در نسخه‌های جدید React (۱۷+)، از یک JSX Transform جدید استفاده می‌شود که باعث می‌شود لازم نباشد import React from "react" در هر فایل باشد. این تغییر ظاهراً ساده، در حجم bundle پروژه‌های بزرگ تفاوت محسوسی می‌سازد. در سطح TS، تایپ‌های JSX به‌طور کامل در زمان کامپایل حذف می‌شوند و به runtime راه پیدا نمی‌کنند. مطالعه‌ی موازی این لایه در بهینه سازی جاوااسکریپت.
  2. React Reconciliation و Identity: الگوریتم Reconciliation React از تایپ‌ها استفاده نمی‌کند اما از ساختار Props و key برای تشخیص تغییرات استفاده می‌کند. یک باگ رایج: تغییر تایپ یک Prop بدون تغییر نام آن. React تشخیص نمی‌دهد که این تغییر نوع است و کامپوننت را دوباره رندر نمی‌کند. TS این را در کامپایل می‌گیرد اگر Props به‌درستی تایپ شده باشند. مطالعه‌ی موازی در مفاهیم پیشرفته جاوااسکریپت.
  3. Strict Mode و Double Rendering: در React Strict Mode، هر کامپوننت دو بار رندر می‌شود تا باگ‌های مربوط به side effectها کشف شود. ترکیب Strict Mode با TypeScript، یک لایه‌ی محافظت اضافه است: TS مطمئن می‌شود که تایپ‌ها در هر دو رندر یکسانند و Strict Mode مطمئن می‌شود که side effectها درست پیاده‌سازی شده‌اند.
  4. React Server Components و TypeScript: در React Server Components (که در Next.js ۱۳+ پشتیبانی می‌شود)، تایپ‌دهی پیچیدگی‌های جدیدی دارد چون بعضی از کدها روی سرور اجرا می‌شوند و بعضی روی کلاینت. TS با تنظیمات خاص مثل "use client" و "use server" می‌تواند این تفکیک را در کامپایل بررسی کند. مطالعه‌ی موازی این لایه در تایپ اسکریپت با Node.js.
  5. Interaction با Bundler و Code Splitting: در bundlerهای مدرن مثل Vite و Webpack، TypeScript به‌تنهایی مسئول build نیست. این bundlerها از اطلاعات تایپ برای بهینه‌سازی (مثل tree-shaking و code splitting) استفاده می‌کنند. اگر Props و Componentها با Named Export و تایپ‌های مشخص تعریف شده باشند، bundler می‌تواند بخش‌های استفاده‌نشده را حذف کند. مطالعه‌ی موازی این لایه در بهینه‌سازی سرعت سایت و ماژول ها در تایپ اسکریپت.

یک تجربه‌ی واقعی از پروژه‌ای که با Reconciliation روبرو شدیم: در یک داشبورد مدیریتی، کامپوننتی داشتیم که بر اساس یک Prop رندر خودش را عوض می‌کرد. بعد از یک refactor، تایپ Prop از "chart" به "graph" تغییر کرد اما کد کامپوننت همچنان روی "chart" چک می‌کرد. به‌دلیل تایپ‌دهی ضعیف در نسخه‌ی قدیم، این خطا از کامپایلر رد شده بود و React هم تشخیص نداده بود. راه‌حل: تقویت تایپ Props به Literal Union و استفاده از الگوی Switch با Default Case که خطا می‌داد اگر تمام حالت‌ها پوشش داده نمی‌شدند. با این تغییر، کامپایلر آن خطا را در همان لحظه گرفت.

اگر روی پروژه‌های وردپرسی هستید و می‌خواهید این لایه‌ها را در development pipeline خود اعمال کنید، پیشنهاد می‌کنم ابتدا به توسعه وردپرس از صفر نگاهی بیندازید. برای مطالعه‌ی موازی با استانداردها و معماری، استانداردهای HTML و CSS و CSS مدرن از Flexbox تا Grid دید وسیع‌تری می‌دهند. برای درک این لایه در چارچوب کارایی، بهینه سازی جاوااسکریپت و بهینه‌سازی سرعت سایت منابع کلیدی هستند. اگر روی موضوع فریم‌ورک‌ها متمرکز هستید، آیا React بهترین انتخاب است و بهترین فریم‌ورک‌های فرانت‌اند دید وسیع‌تری می‌دهند. اگر هم به سمت معماری و ابزار می‌روید، ترندهای معماری وب و ابزارهای CI/CD منابع کلیدی هستند.

React یک کتابخانه است که به یک قرارداد وفادار می‌ماند؛ TypeScript ابزاری است که این قرارداد را در سطح کد محکم می‌کند. تفاوت بین یک پروژه‌ی React معمولی و یک پروژه‌ی React حرفه‌ای، در همین قرارداد پنهان است.

ایستگاه پایانی این مسیر

ترکیب TypeScript و React را می‌توان در یک جمله خلاصه کرد: «ابزاری برای محافظت از قراردادهای React در سطح کد، از Props تا State و Context.» سه درس که از این مسیر با خودم بردم:

  1. Props را به‌عنوان قرارداد ببینید. هر کامپوننت، یک تابع با یک ورودی مشخص است. اگر Props را با دقت تایپ کنید، کامپایلر به یک محافظ قوی تبدیل می‌شود. اگر نه، TypeScript فقط یک لایه‌ی ظاهری است.
  2. Event Handlerها را جدی بگیرید. این‌ها پرکاربردترین نقاط تماس بین کد شما و کاربر هستند. استفاده از تایپ‌های مشخص React (مثل React.ChangeEvent<T>) به‌جای any، اولین قدم در این مسیر است.
  3. الگوهای خودتان را بسازید. Custom Hookها، Context API و Generics در React فرصت‌های بزرگی برای ساختن الگوهای تکراری با تایپ‌دهی دقیق هستند. یک Custom Hook خوب، می‌تواند در سراسر پروژه یک قرارداد محکم بسازد.

مسیر یادگیری فرانت‌اند با این نوشته تمام نمی‌شود. اگر می‌خواهید مرحله‌ی بعدی را بردارید، هوک‌های React، آیا React بهترین انتخاب است و بهترین فریم‌ورک‌های فرانت‌اند سه قدم منطقی بعدی هستند. اگر روی مفاهیم TypeScript متمرکز هستید، آموزش تایپ اسکریپت از صفر، جنریک در تایپ اسکریپت و اینترفیس در تایپ اسکریپت منابع کلیدی هستند. اگر هم به سمت ابزار و زیرساخت می‌روید، تایپ اسکریپت با Node.js، تنظیمات tsconfig و ابزارهای CI/CD دید وسیع‌تری می‌دهند.

React + TypeScript همیشه یکی از آن ترکیب‌هایی است که در ابتدا کمی پیچیده به‌نظر می‌رسد اما بعد از چند ماه کار با آن، بازگشت به React خالص تقریباً غیرممکن می‌شود. اگر شما هم تجربه‌ای از یک تایپ‌دهی ناقص دارید که بعداً به باگ production تبدیل شد — یا از یک الگوی Custom Hook که کل پروژه را ساده‌تر کرد — آن تجربه را برای ما تعریف کنید. آن نوع داستان‌ها، برای کسی که امروز در حال شروع یک پروژه‌ی React با TypeScript است، ارزش عملی بیشتری از هر مستند رسمی دارند.