تایپ اسکریپت با React: توسعه کامپوننت‌های Type-safe و پایدار

فهرست مطالب

تایپ اسکریپت با React: توسعه کامپوننت‌های Type-safe و پایدار

در دنیای پویای توسعه وب فرانت‌اند، React به عنوان یکی از محبوب‌ترین کتابخانه‌های ساخت رابط کاربری، جایگاه ویژه‌ای پیدا کرده است. از سوی دیگر، TypeScript به عنوان یک اَبَر مجموعه (Superset) بر پایه جاوااسکریپت، با افزودن سیستم نوع‌بندی استاتیک، تحولی عظیم در کیفیت و مقیاس‌پذیری پروژه‌های بزرگ ایجاد کرده است. ترکیب این دو فناوری قدرتمند، یعنی React و TypeScript، به توسعه‌دهندگان این امکان را می‌دهد که کامپوننت‌هایی با اطمینان‌پذیری بالا، قابلیت نگهداری بیشتر و خطاهای زمان اجرا (Runtime Errors) کمتر توسعه دهند.

این مقاله جامع برای توسعه‌دهندگانی طراحی شده است که قصد دارند دانش خود را در زمینه ترکیب React و TypeScript عمیق‌تر کنند و پروژه‌های خود را به سطح بالاتری از پایداری و Type-safety برسانند. ما از مفاهیم پایه‌ای شروع کرده و تا الگوهای پیشرفته‌تر پیش خواهیم رفت، با هدف ارائه یک راهنمای کامل و عملی برای ساخت کامپوننت‌های قدرتمند و Type-safe.

چرا Type-safety با React ضروری است؟

جاوااسکریپت به دلیل ماهیت پویا و بدون نوع‌بندی خود، در پروژه‌های بزرگ و پیچیده می‌تواند منجر به خطاهای غیرمنتظره و زمان‌بر شود. این خطاها اغلب در زمان اجرا و پس از استقرار برنامه آشکار می‌شوند که کشف و رفع آن‌ها پرهزینه است. TypeScript با معرفی سیستم نوع‌بندی استاتیک، این چالش‌ها را به شکل چشمگیری کاهش می‌دهد. اما چرا این ویژگی برای React که خود بر پایه جاوااسکریپت است، اینقدر حیاتی است؟

۱. کاهش خطاهای زمان اجرا (Runtime Errors)

مهم‌ترین مزیت استفاده از TypeScript، تشخیص و رفع خطاها در زمان کامپایل (Compile Time) است، قبل از اینکه کد شما به مرورگر برسد. در React، این به معنای اطمینان از اینکه Props‌های ارسال شده به کامپوننت‌ها، State مدیریت شده، و مقادیر برگشتی از Hooks، همگی مطابق با انتظارات تعریف شده هستند. به عنوان مثال، اگر یک کامپوننت انتظار یک عدد را برای Prop خاصی داشته باشد و به اشتباه یک رشته به آن ارسال شود، TypeScript فوراً این خطا را گزارش می‌کند. این امر به طور قابل توجهی خطاهای رایج مانند `undefined is not a function` یا `Cannot read property ‘x’ of undefined` را کاهش می‌دهد.

۲. بهبود تجربه توسعه‌دهنده (Developer Experience – DX)

IDEها (مانند VS Code) با پشتیبانی کامل از TypeScript، قابلیت‌های پیشرفته‌ای نظیر تکمیل خودکار کد (Autocompletion)، بررسی خطا در لحظه (Live Error Checking)، و ابزارهای Refactoring ایمن را ارائه می‌دهند. وقتی Props یک کامپوننت را تعریف می‌کنید، IDE به شما کمک می‌کند تا با تایپ صحیح آن‌ها را استفاده کنید، و حتی هنگام Refactoring یک نام Prop، تمام ارجاعات به آن به طور خودکار و ایمن به‌روزرسانی می‌شوند. این ویژگی‌ها بهره‌وری تیم را بالا برده و زمان صرف شده برای یافتن خطاهای ساده را به حداقل می‌رسانند.

۳. نگهداری و مقیاس‌پذیری بهتر کد

در پروژه‌های بزرگ با ده‌ها یا صدها کامپوننت، درک اینکه هر کامپوننت چه Propهایی را می‌پذیرد و چه انتظاراتی از داده‌ها دارد، بدون نوع‌بندی صریح می‌تواند دشوار باشد. TypeScript به عنوان یک مستندات زنده برای کد عمل می‌کند. هر توسعه‌دهنده‌ای که به یک کامپوننت نگاه می‌کند، فوراً متوجه می‌شود که چگونه باید از آن استفاده کند و چه داده‌هایی را باید به آن ارسال کند. این شفافیت، فرآیند نگهداری را آسان‌تر کرده و توسعه ویژگی‌های جدید را در یک codebase بزرگ و پیچیده، مقیاس‌پذیرتر می‌سازد.

۴. همکاری تیمی آسان‌تر

در تیم‌های توسعه، TypeScript به عنوان یک زبان مشترک عمل می‌کند که انتظارات داده‌ای را برای همه روشن می‌کند. این موضوع از سوءتفاهم‌ها و خطاهای ناشی از عدم تطابق داده‌ها جلوگیری می‌کند. توسعه‌دهندگان می‌توانند با اطمینان بیشتری روی بخش‌های مختلف پروژه کار کنند، زیرا سیستم نوع‌بندی TypeScript به عنوان یک دروازه امنیتی عمل می‌کند که از تزریق داده‌های نادرست جلوگیری می‌نماید.

۵. مستندسازی از طریق نوع‌بندی

تعریف نوع‌ها برای Propها، State، و توابع در React، به خودی خود نوعی مستندسازی است. این مستندسازی همیشه به‌روز و دقیق است، زیرا در صورت عدم تطابق با کد، TypeScript خطا می‌دهد. این امر نیاز به مستندات جداگانه و دستی را کاهش داده و تضمین می‌کند که مستندات کد همیشه با واقعیت منطبق هستند.

با درک این مزایا، روشن می‌شود که چرا Type-safety، به ویژه در اکوسیستم React، نه تنها یک گزینه، بلکه یک ضرورت برای توسعه نرم‌افزارهای پایدار و با کیفیت بالا است.

تنظیم پروژه React-TypeScript شما

برای شروع توسعه کامپوننت‌های Type-safe با React و TypeScript، ابتدا باید یک محیط توسعه مناسب راه‌اندازی کنید. دو روش اصلی برای انجام این کار وجود دارد: استفاده از Create React App (CRA) با قالب TypeScript، یا افزودن TypeScript به یک پروژه React موجود.

۱. راه‌اندازی پروژه جدید با Create React App (CRA)

ساده‌ترین راه برای شروع یک پروژه جدید React با TypeScript، استفاده از Create React App است. CRA یک محیط توسعه آماده با تنظیمات بهینه برای React فراهم می‌کند و با استفاده از پرچم `–template typescript`، TypeScript را به طور خودکار پیکربندی می‌کند.

npx create-react-app my-type-safe-app --template typescript
cd my-type-safe-app
npm start

یا با استفاده از Yarn:

yarn create react-app my-type-safe-app --template typescript
cd my-type-safe-app
yarn start

این دستورات یک پروژه React جدید ایجاد می‌کنند که شامل فایل‌های پیکربندی TypeScript (`tsconfig.json`) و وابستگی‌های لازم است. شما می‌توانید بلافاصله شروع به نوشتن کامپوننت‌های React با TypeScript کنید.

۲. افزودن TypeScript به پروژه React موجود

اگر پروژه‌ React موجودی دارید که مایل به اضافه کردن TypeScript به آن هستید، مراحل کمی متفاوت است. شما نیاز به نصب پکیج‌های TypeScript و نوع‌بندی (Type Definitions) React دارید.

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

یا با Yarn:

yarn add --dev typescript @types/react @types/react-dom @types/node

پس از نصب، باید یک فایل `tsconfig.json` در ریشه پروژه خود ایجاد کنید. این فایل شامل تنظیمات کامپایلر TypeScript است. می‌توانید با اجرای دستور زیر آن را به صورت خودکار ایجاد کنید:

npx tsc --init

سپس محتوای آن را برای یک پروژه React بهینه‌سازی کنید. یک پیکربندی پایه برای `tsconfig.json` می‌تواند به شکل زیر باشد:

{
  "compilerOptions": {
    "target": "es2016",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}

توضیحات مختصر برای برخی از مهم‌ترین گزینه‌ها:

  • target: نسخه ECMAScript هدف برای کدهای کامپایل شده (مثلاً es2016).
  • lib: کتابخانه‌هایی که در دسترس هستند (مثلاً dom برای APIهای مرورگر).
  • jsx: نحوه تبدیل JSX (برای React از react-jsx یا react استفاده کنید).
  • strict: فعال کردن تمام بررسی‌های نوع‌بندی سخت‌گیرانه (بسیار توصیه می‌شود).
  • esModuleInterop: فعال کردن سازگاری با ماژول‌های CommonJS و ES Modules.
  • skipLibCheck: رد کردن بررسی نوع‌بندی برای فایل‌های تعریف نوع کتابخانه‌ها (برای سرعت).

در نهایت، باید پسوند فایل‌های React خود را از `.js` یا `.jsx` به `.ts` یا `.tsx` تغییر دهید تا TypeScript شروع به بررسی آن‌ها کند. برای مثال، `src/index.js` به `src/index.tsx` و `src/App.js` به `src/App.tsx` تبدیل می‌شوند.

با این تنظیمات، شما آماده‌اید تا از قدرت TypeScript در پروژه React خود بهره‌مند شوید.

تعریف Props و State کامپوننت‌ها با TypeScript

یکی از مهم‌ترین جنبه‌های Type-safety در React، تعریف دقیق Props و State کامپوننت‌ها است. TypeScript به ما این امکان را می‌دهد که ساختار داده‌هایی که به کامپوننت‌ها ارسال می‌شوند یا توسط آن‌ها مدیریت می‌شوند را به وضوح مشخص کنیم.

۱. تعریف Props برای کامپوننت‌های تابعی (Functional Components)

اکثر کامپوننت‌های React امروزه به صورت تابعی نوشته می‌شوند. برای تعریف Props آن‌ها، می‌توانید از Interface یا Type Alias استفاده کنید. Type Alias معمولاً برای تعریف انواع ساده‌تر یا ترکیبی استفاده می‌شود، در حالی که Interface برای تعریف شکل شیء (Object Shape) یا کلاس‌ها کاربرد دارد و می‌تواند Extend شود. در عمل، هر دو برای Props کار می‌کنند و انتخاب بین آن‌ها اغلب سلیقه‌ای است.

// استفاده از Interface
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean; // Prop اختیاری
}

const Button: React.FC<ButtonProps> = ({ label, onClick, disabled }) => {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
};

// استفاده از Type Alias
type CardProps = {
  title: string;
  content: string;
  footer?: React.ReactNode; // مثال برای Children یا JSX
};

const Card = ({ title, content, footer }: CardProps) => {
  return (
    <div>
      <h3>{title}</h3>
      <p>{content}</p>
      {footer && <div>{footer}</div>}
    </div>
  );
};

نکات مهم:

  • React.FC<Props>: این نوع، به React.FunctionComponent نیز معروف است و برای تعریف کامپوننت‌های تابعی به همراه Props آن‌ها استفاده می‌شود. این نوع به طور خودکار Prop children را شامل می‌شود. با این حال، استفاده از آن کاملاً ضروری نیست و برخی توسعه‌دهندگان ترجیح می‌دهند Props را به صورت مستقیم به تابع پاس دهند (مانند مثال Card).
  • ? (Optional Props): با افزودن ? پس از نام Prop، آن را اختیاری می‌کنید. اگر Prop ارسال نشود، مقدار آن undefined خواهد بود.
  • children Prop: اگر کامپوننت شما محتوایی را به عنوان Children می‌پذیرد، می‌توانید آن را به صورت صریح تعریف کنید. React.ReactNode یک نوع عمومی است که شامل هر چیزی است که React می‌تواند render کند (JSX، رشته، عدد، Fragment، پورتال، بولین، null یا undefined).
interface ContainerProps {
  children: React.ReactNode;
  className?: string;
}

const Container: React.FC<ContainerProps> = ({ children, className }) => {
  return <div className={className}>{children}</div>;
};

۲. تعریف State برای کامپوننت‌های کلاسی (Class Components)

اگرچه کامپوننت‌های تابعی با Hooks بیشتر مورد استفاده قرار می‌گیرند، اما آشنایی با تعریف Props و State برای کامپوننت‌های کلاسی نیز مفید است. کامپوننت‌های کلاسی از جنریک‌ها (Generics) برای تعریف نوع Props و State خود استفاده می‌کنند.

interface CounterProps {
  initialCount?: number;
}

interface CounterState {
  count: number;
}

class Counter extends React.Component<CounterProps, CounterState> {
  constructor(props: CounterProps) {
    super(props);
    this.state = {
      count: props.initialCount || 0,
    };
  }

  increment = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1,
    }));
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

در اینجا، CounterProps نوع Props و CounterState نوع State را تعریف می‌کند. React.Component<CounterProps, CounterState> این نوع‌ها را به کامپوننت کلاسی اعمال می‌کند.

۳. مدیریت Default Props

در React، گاهی اوقات می‌خواهید مقادیر پیش‌فرض برای Props تعریف کنید. TypeScript به شما این امکان را می‌دهد که با ترکیب نوع‌ها، این کار را به خوبی انجام دهید.

interface GreetingProps {
  name: string;
  message?: string;
}

const Greeting: React.FC<GreetingProps> = ({ name, message = "Hello" }) => {
  return <div>{message}, {name}!</div>;
};

// استفاده:
// <Greeting name="Alice" />  // خروجی: Hello, Alice!
// <Greeting name="Bob" message="Hi" /> // خروجی: Hi, Bob!

در این مثال، message به عنوان یک Prop اختیاری تعریف شده است، اما با استفاده از مقدار پیش‌فرض در دیس‌ساختارینگ (Destructuring)، تضمین می‌شود که همیشه یک مقدار رشته‌ای داشته باشد. TypeScript به خوبی این موضوع را درک می‌کند و نوع message را به صورت string (نه string | undefined) در بدنه کامپوننت infer می‌کند.

با تعریف دقیق Props و State، شما اطمینان حاصل می‌کنید که داده‌ها در سراسر برنامه شما سازگار و قابل پیش‌بینی هستند، که این امر به شدت به پایداری و نگهداری کد کمک می‌کند.

کار با Hooks در TypeScript: useState, useEffect, useRef و useContext

Hooks انقلابی در توسعه کامپوننت‌های تابعی React ایجاد کرده‌اند. TypeScript به طور عالی با Hooks ادغام می‌شود و به شما امکان می‌دهد State، Side Effects، Refها و Context را با امنیت نوع‌بندی مدیریت کنید.

۱. useState

useState برای مدیریت State در کامپوننت‌های تابعی استفاده می‌شود. TypeScript معمولاً می‌تواند نوع State را از مقدار اولیه infer کند، اما می‌توانید نوع را به صورت صریح نیز تعریف کنید.

// inferring type from initial value (string)
const [name, setName] = useState(""); // name: string

// inferring type from initial value (number or null)
const [userId, setUserId] = useState<number | null>(null); // userId: number | null

// Explicitly defining type for an object
interface User {
  id: number;
  name: string;
  email: string;
}

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

// برای به‌روزرسانی state نیز نوع‌بندی اعمال می‌شود:
setUser({ id: 1, name: "Alice", email: "alice@example.com" }); // OK
// setUser({ id: 1, name: "Alice" }); // Error: Property 'email' is missing

هنگام استفاده از مقادیر اولیه پیچیده یا زمانی که State می‌تواند چندین نوع داشته باشد (مانند number | null)، تعریف صریح نوع با استفاده از جنریک‌ها (useState<Type>) توصیه می‌شود.

۲. useEffect

useEffect برای مدیریت Side Effects استفاده می‌شود و معمولاً نیازی به تعریف نوع صریح ندارد، زیرا TypeScript می‌تواند نوع پارامترها و مقادیر برگشتی آن را به درستی infer کند.

const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `You clicked ${count} times`;
  return () => {
    // Cleanup function
  };
}, [count]); // Dependency array

نکته مهم در useEffect، اطمینان از صحت آرایه وابستگی‌ها (Dependency Array) است. TypeScript به طور مستقیم در این مورد کمک نمی‌کند، اما ابزارهای linting مانند ESLint می‌توانند مشکلات وابستگی را شناسایی کنند.

۳. useRef

useRef برای دسترسی به المان‌های DOM یا نگهداری هر مقدار قابل تغییر در طول عمر کامپوننت استفاده می‌شود.

// For a DOM element
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
  if (inputRef.current) {
    inputRef.current.focus();
  }
}, []);

// For a generic mutable value
const timerId = useRef<number | null>(null);

useEffect(() => {
  timerId.current = window.setInterval(() => {
    // do something
  }, 1000);

  return () => {
    if (timerId.current) {
      clearInterval(timerId.current);
    }
  };
}, []);

همیشه مقدار اولیه useRef را به null تنظیم کنید و نوع آن را به صورت HTMLInputElement | null یا number | null تعریف کنید، زیرا current در ابتدا null است تا زمانی که المان DOM (یا مقدار) به آن اختصاص داده شود.

۴. useContext

useContext برای مصرف مقادیر از React Context استفاده می‌شود. تعریف نوع برای Context بسیار مهم است تا اطمینان حاصل شود که داده‌های مورد انتظار را دریافت می‌کنید.

interface ThemeContextType {
  theme: "light" | "dark";
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// Theme Provider
const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
  };

  const contextValue = useMemo(() => ({ theme, toggleTheme }), [theme]);

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
};

// Consuming the context
const ThemeButton: React.FC = () => {
  const themeContext = useContext(ThemeContext);

  if (!themeContext) {
    throw new Error("ThemeButton must be used within a ThemeProvider");
  }

  const { theme, toggleTheme } = themeContext;

  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
};

در مثال ThemeContext، مقدار اولیه Context را undefined قرار داده‌ایم و نوع آن را ThemeContextType | undefined تعریف کرده‌ایم. این کار از مشکلاتی که هنگام دسترسی به Context قبل از مقداردهی اولیه رخ می‌دهد، جلوگیری می‌کند. در کامپوننت مصرف‌کننده (ThemeButton)، ما یک بررسی برای !themeContext اضافه کرده‌ایم تا اطمینان حاصل کنیم که کامپوننت در داخل یک ThemeProvider قرار دارد و خطاهای زمان اجرا را کاهش دهیم.

۵. useReducer

useReducer برای مدیریت State پیچیده‌تر که شامل منطق به‌روزرسانی پیچیده است، استفاده می‌شود. تعریف نوع برای State و Action بسیار مهم است.

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

type TodoAction =
  | { type: "ADD_TODO"; text: string }
  | { type: "TOGGLE_TODO"; id: number }
  | { type: "REMOVE_TODO"; id: number };

function todoReducer(state: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [
        ...state,
        { id: Date.now(), text: action.text, completed: false },
      ];
    case "TOGGLE_TODO":
      return state.map((todo) =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    case "REMOVE_TODO":
      return state.filter((todo) => todo.id !== action.id);
    default:
      return state;
  }
}

const TodoList: React.FC = () => {
  const [todos, dispatch] = useReducer(todoReducer, []);

  const addTodo = (text: string) => {
    dispatch({ type: "ADD_TODO", text });
  };

  return (
    <div>
      <input
        type="text"
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            addTodo(e.currentTarget.value);
            e.currentTarget.value = "";
          }
        }}
        placeholder="Add a new todo"
      />
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
              onClick={() => dispatch({ type: "TOGGLE_TODO", id: todo.id })}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: "REMOVE_TODO", id: todo.id })}>
              Remove
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

با تعریف TodoAction به عنوان یک Union Type از تمام اکشن‌های ممکن، TypeScript می‌تواند بررسی کند که آیا اکشن‌های ارسال شده به dispatch معتبر هستند یا خیر. این کار به جلوگیری از خطاهای ناشی از اکشن‌های اشتباه کمک می‌کند.

الگوهای پیشرفته TypeScript در React

TypeScript فراتر از Type-safety پایه‌ای برای Props و State عمل می‌کند و الگوهای پیشرفته‌ای را ارائه می‌دهد که به شما کمک می‌کنند کامپوننت‌های انعطاف‌پذیرتر و قابل استفاده مجدد ایجاد کنید.

۱. کامپوننت‌های Polymorphic و Generic

گاهی اوقات می‌خواهید یک کامپوننت داشته باشید که بتواند به عنوان المان‌های مختلف HTML رندر شود، اما همچنان Props مربوط به آن المان را بپذیرد. اینجاست که کامپوننت‌های Polymorphic وارد می‌شوند.

// Generic (Polymorphic) Button Component
import React from 'react';

// 1. Define a type for the component's default props
interface ButtonProps<T extends React.ElementType> {
  as?: T; // Allows the component to be rendered as a different HTML element
  children: React.ReactNode;
  className?: string;
  // Add other common props you expect
}

// 2. Define props for the specific element that `as` represents
// This uses React.ComponentPropsWithoutRef to infer props for the element,
// and then Omit to remove props that are already defined in ButtonProps
type PolymorphicComponentProps<T extends React.ElementType, P = {}> =
  React.PropsWithChildren<P & ButtonProps<T>> &
  Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonProps<T>>;

// 3. Create the component function
// React.forwardRef allows forwarding refs to the underlying DOM element
const Button = React.forwardRef(
  <T extends React.ElementType = 'button'>(
    { as, children, className, ...rest }: PolymorphicComponentProps<T>,
    ref?: React.ComponentPropsWithRef<T>['ref']
  ) => {
    const Component = as || 'button';
    return (
      <Component ref={ref} className={className} {...rest}>
        {children}
      </Component>
    );
  }
);

// This ensures type safety when using the component
// Example usage:
const App: React.FC = () => {
  return (
    <div>
      <Button onClick={() => alert("Clicked!")}>
        Regular Button
      </Button>
      <Button as="a" href="https://example.com" target="_blank">
        Link Button
      </Button>
      <Button as="div" style={{ padding: '10px', border: '1px solid black' }}>
        Div Button
      </Button>
    </div>
  );
};

این الگو کمی پیچیده است، اما بسیار قدرتمند. as?: T به کامپوننت اجازه می‌دهد به عنوان یک المان HTML دیگر رندر شود. PolymorphicComponentProps از Generics برای ترکیب Props پیش‌فرض کامپوننت با Propsهای خاص المان HTML استفاده می‌کند و از Omit برای جلوگیری از تداخل نام Props استفاده می‌کند.

۲. Utility Types در TypeScript

TypeScript مجموعه‌ای از Utility Types داخلی را فراهم می‌کند که به شما در ساخت انواع پیچیده کمک می‌کنند. اینها برای استفاده با React نیز مفید هستند:

  • Partial<T>: تمام ویژگی‌های T را اختیاری می‌کند.
  • Pick<T, K>: زیرمجموعه‌ای از ویژگی‌های T را بر اساس کلیدهای K انتخاب می‌کند.
  • Omit<T, K>: زیرمجموعه‌ای از ویژگی‌های T را با حذف کلیدهای K انتخاب می‌کند.
  • Exclude<T, U>: انواع U را از T حذف می‌کند.
  • NonNullable<T>: انواع null و undefined را از T حذف می‌کند.
  • ReturnType<T>: نوع مقدار برگشتی یک تابع را استخراج می‌کند.
  • Parameters<T>: نوع پارامترهای یک تابع را به صورت یک تاپل استخراج می‌کند.
// Example with Pick and Omit
interface UserProfile {
  id: number;
  name: string;
  email: string;
  avatarUrl: string;
}

type UserPreviewProps = Pick<UserProfile, "name" | "avatarUrl">;

const UserPreview: React.FC<UserPreviewProps> = ({ name, avatarUrl }) => {
  return (
    <div>
      <img src={avatarUrl} alt={name} />
      <p>{name}</p>
    </div>
  );
};

type UserFormProps = Omit<UserProfile, "id" | "avatarUrl">;

const UserForm: React.FC<UserFormProps> = ({ name, email }) => {
  // ... form logic
  return (
    <form>
      <input type="text" value={name} />
      <input type="email" value={email} />
      <button type="submit">Save</button>
    </form>
  );
};

این Utility Types به شما کمک می‌کنند تا انواع داده‌ای را از انواع موجود مشتق کنید و از تکرار کد جلوگیری کنید.

۳. Higher-Order Components (HOCs) با TypeScript

اگرچه Hooks به طور فزاینده‌ای جایگزین HOCs می‌شوند، اما HOCs هنوز در برخی سناریوها کاربرد دارند. تایپ کردن HOCها می‌تواند چالش‌برانگیز باشد.

// A simple HOC for logging props
function withLogger<P extends object>(Component: React.ComponentType<P>) {
  type ComponentProps = React.ComponentProps<typeof Component>;

  const WrapperComponent: React.FC<ComponentProps> = (props) => {
    useEffect(() => {
      console.log("Props:", props);
    }, [props]);

    return <Component {...props} />;
  };

  WrapperComponent.displayName = `withLogger(${Component.displayName || Component.name})`;

  return WrapperComponent;
}

interface MyComponentProps {
  message: string;
  count: number;
}

const MyComponent: React.FC<MyComponentProps> = ({ message, count }) => {
  return <div>{message}: {count}</div>;
};

const LoggedMyComponent = withLogger(MyComponent);

// Usage: <LoggedMyComponent message="Hello" count={5} />

در این مثال، withLogger یک تابع جنریک است که نوع Props کامپوننت ورودی را حفظ می‌کند. P extends object به TypeScript می‌گوید که P باید یک شیء باشد (که تمام Propsها از آن مشتق می‌شوند). React.ComponentProps<typeof Component> نوع Props کامپوننت wrapped شده را استخراج می‌کند.

۴. Render Props با TypeScript

الگوی Render Props نیز راهی برای به اشتراک گذاشتن کد بین کامپوننت‌ها است. تایپ کردن آن در TypeScript نسبتاً ساده است.

interface DataFetcherProps<T> {
  url: string;
  render: (data: T | null, isLoading: boolean, error: Error | null) => React.ReactNode;
}

function DataFetcher<T>({ url, render }: DataFetcherProps<T>) {
  const [data, setData] = useState<T | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result: T = await response.json();
        setData(result);
      } catch (err: any) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return <>{render(data, isLoading, error)}</>;
}

interface User {
  id: number;
  name: string;
}

const UserList: React.FC = () => {
  return (
    <DataFetcher<User[]>
      url="https://jsonplaceholder.typicode.com/users"
      render={(users, loading, error) => {
        if (loading) return <p>Loading users...</p>;
        if (error) return <p style={{ color: "red" }}>Error: {error.message}</p>;
        if (!users) return <p>No users found.</p>;

        return (
          <ul>
            {users.map((user) => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        );
      }}
    />
  );
};

با استفاده از Generics برای DataFetcher، می‌توانیم مشخص کنیم که چه نوع داده‌ای (T) از API انتظار می‌رود، و تابع render نیز از این نوع استفاده می‌کند تا Type-safety را در زمان استفاده از داده‌ها تضمین کند.

بهترین روش‌ها برای توسعه Type-Safe در React

برای حداکثر بهره‌وری از ترکیب React و TypeScript، پیروی از مجموعه‌ای از بهترین روش‌ها می‌تواند تفاوت چشمگیری در کیفیت و مقیاس‌پذیری کد ایجاد کند.

۱. فعال کردن Strict Mode در tsconfig.json

یکی از مهمترین گام‌ها، فعال کردن "strict": true در فایل tsconfig.json است. این گزینه تمام بررسی‌های Type-safety سخت‌گیرانه را فعال می‌کند، از جمله:

  • noImplicitAny: جلوگیری از استفاده از any در مواردی که TypeScript نمی‌تواند نوع را infer کند.
  • strictNullChecks: اجبار به بررسی صریح مقادیر null و undefined.
  • strictFunctionTypes: بررسی دقیق‌تر امضای توابع.
  • strictPropertyInitialization: اطمینان از مقداردهی اولیه Propertyهای کلاس.

اگرچه در ابتدا ممکن است با خطاهای بیشتری مواجه شوید، اما این کار به شما کمک می‌کند کدی بسیار با کیفیت‌تر و بدون خطای زمان اجرا بنویسید.

۲. از Any اجتناب کنید (حتی الامکان)

استفاده از any تقریباً تمام مزایای TypeScript را از بین می‌برد. در مواقعی که مجبور به استفاده از آن هستید (مثلاً هنگام تعامل با کتابخانه‌های بدون تعریف نوع یا داده‌های API غیرقابل پیش‌بینی)، سعی کنید محدوده any را به حداقل برسانید. در بیشتر موارد، راه‌هایی برای تعریف دقیق‌تر نوع‌ها وجود دارد، حتی اگر به معنای کمی تلاش بیشتر باشد.

۳. از Inference (استنتاج نوع) به درستی استفاده کنید

TypeScript بسیار باهوش است و می‌تواند بسیاری از نوع‌ها را به طور خودکار Infer کند. همیشه نیازی به Type کردن صریح هر متغیر یا پارامتر نیست. از قابلیت‌های Inference TypeScript بهره ببرید تا کد شما خواناتر و کمتر verbose شود. با این حال، در مرزهای سیستم (مثلاً Props کامپوننت، ورودی/خروجی API)، صریح بودن بسیار مهم است.

۴. سازماندهی نوع‌ها

با بزرگ شدن پروژه، تعداد Interfaceها و Type Aliasها نیز افزایش می‌یابد. بهتر است این نوع‌ها را در فایل‌های جداگانه (مثلاً `types.ts` یا `interfaces.ts`) یا در کنار کامپوننت‌هایی که از آن‌ها استفاده می‌کنند، سازماندهی کنید. برای کامپوننت‌های بزرگتر یا نوع‌های مشترک، ایجاد یک دایرکتوری `types/` در ریشه پروژه می‌تواند مفید باشد.

۵. تایپ کردن Event Handlers

وقتی با Eventهای DOM در React کار می‌کنید، مطمئن شوید که Event Listenerها را به درستی تایپ کرده‌اید. React انواع Event خاص خود را دارد که می‌توانید از آن‌ها استفاده کنید.

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

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
  console.log("Button clicked!");
};

return (
  <div>
    <input type="text" onChange={handleChange} />
    <button onClick={handleClick}>Click me</button>
  </div>
);

انواع رایج عبارتند از: React.ChangeEvent، React.MouseEvent، React.FormEvent، React.KeyboardEvent. شما می‌توانید المان هدف را نیز با Generics (مثلاً HTMLInputElement) مشخص کنید.

۶. استفاده از لینتر (Linter) با پشتیبانی از TypeScript

ابزارهایی مانند ESLint با پلاگین‌های TypeScript (مانند `@typescript-eslint/eslint-plugin`) می‌توانند به شما در اعمال بهترین روش‌ها و شناسایی مشکلات قبل از کامپایل کمک کنند. این ابزارها می‌توانند خطاهای احتمالی را شناسایی کرده و سبک کدگذاری را یکنواخت کنند.

۷. نوشتن تست‌های Type-Safe

تست‌هایی که برای کامپوننت‌های React خود می‌نویسید، می‌توانند از Type-safety بهره‌مند شوند. استفاده از کتابخانه‌های تست مانند React Testing Library یا Jest با TypeScript، اطمینان می‌دهد که Props‌های ارسال شده به کامپوننت‌ها در تست‌ها نیز صحیح هستند.

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

عیب‌یابی خطاهای رایج TypeScript در React

هنگام کار با TypeScript و React، احتمالاً با برخی از خطاهای رایج TypeScript روبرو خواهید شد. درک این خطاها و نحوه رفع آن‌ها می‌تواند زمان توسعه شما را به شدت بهبود بخشد.

۱. Property ‘X’ does not exist on type ‘Y’

این خطا یکی از رایج‌ترین خطاها است و نشان می‌دهد که شما سعی دارید به یک ویژگی (Property) دسترسی پیدا کنید که در نوع تعریف شده وجود ندارد.

interface User {
  name: string;
  age: number;
}

const user: User = { name: "Alice", age: 30 };
// console.log(user.email); // Error: Property 'email' does not exist on type 'User'.

راه حل:

  • اگر ویژگی واقعاً وجود ندارد: آن را به Interface یا Type Alias اضافه کنید.
  • اگر ویژگی اختیاری است: از عملگر ? استفاده کنید (مثلاً email?: string;).
  • اگر می‌دانید که ویژگی در زمان اجرا وجود خواهد داشت (مثلاً از یک پاسخ API): می‌توانید از Optional Chaining (?.) یا Non-null Assertion Operator (!) استفاده کنید، اما با احتیاط.

۲. Type ‘A’ is not assignable to type ‘B’

این خطا نشان می‌دهد که شما سعی دارید مقداری از یک نوع را به متغیری که نوع دیگری دارد، اختصاص دهید. این خطا معمولاً هنگام ارسال Props با نوع اشتباه رخ می‌دهد.

interface ButtonProps {
  label: string;
  onClick: () => void;
}

const MyButton: React.FC<ButtonProps> = ({ label, onClick }) => {
  return <button onClick={onClick}>{label}</button>;
};

// <MyButton label={123} onClick={() => {}} /> // Error: Type 'number' is not assignable to type 'string'.

راه حل:

  • مطمئن شوید که نوع داده‌ای که اختصاص می‌دهید با نوع تعریف شده مطابقت دارد.
  • اگر داده از یک منبع خارجی (مثل API) می‌آید، مطمئن شوید که آن را به نوع صحیح نگاشت کرده‌اید.
  • از Union Types (TypeA | TypeB) استفاده کنید اگر متغیر می‌تواند چندین نوع مختلف را نگه دارد.

۳. Object is possibly ‘null’ or ‘undefined’

این خطا به دلیل فعال بودن strictNullChecks رخ می‌دهد و به شما هشدار می‌دهد که متغیری که به آن دسترسی پیدا می‌کنید، ممکن است null یا undefined باشد. این یک ویژگی امنیتی TypeScript است.

const myRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  // console.log(myRef.current.clientWidth); // Error: Object is possibly 'null'.
  if (myRef.current) {
    console.log(myRef.current.clientWidth); // OK
  }
}, []);

راه حل:

  • قبل از دسترسی، بررسی null/undefined انجام دهید (مانند مثال بالا با if (myRef.current)).
  • از Optional Chaining (myRef.current?.clientWidth) استفاده کنید اگر می‌خواهید در صورت null/undefined بودن، عملیات متوقف شود و undefined برگردانده شود.
  • در مواردی که مطمئن هستید مقدار null/undefined نیست، می‌توانید از Non-null Assertion Operator (!) استفاده کنید (مثلاً myRef.current!.clientWidth)، اما این کار را با احتیاط و فقط زمانی که کاملاً مطمئن هستید، انجام دهید.

۴. Argument of type ‘X’ is not assignable to parameter of type ‘Y’ (for Event Handlers)

این خطا اغلب زمانی رخ می‌دهد که شما یک Event Handler را به درستی تایپ نکرده‌اید.

// Incorrect:
const handleChange = (e: React.FormEvent) => {
  // console.log(e.target.value); // Error: Property 'value' does not exist on type 'EventTarget'.
};

// Correct:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value); // OK
};

// <input type="text" onChange={handleChange} />

راه حل:

  • همیشه از انواع Event صحیح React استفاده کنید (React.ChangeEvent، React.MouseEvent، و غیره).
  • پارامتر Generic مربوط به نوع المان را در صورت لزوم (مثلاً HTMLInputElement) مشخص کنید تا به ویژگی‌های خاص آن المان (مانند value یا checked) دسترسی داشته باشید.

۵. Type ‘X’ has no compatible call signatures (for function props)

این خطا زمانی رخ می‌دهد که یک تابع را به عنوان Prop ارسال می‌کنید، اما امضای آن (پارامترها و نوع برگشتی) با آنچه در Prop تعریف شده است، مطابقت ندارد.

interface ChildProps {
  onSave: (data: { name: string; age: number }) => void;
}

const ChildComponent: React.FC<ChildProps> = ({ onSave }) => {
  const handleClick = () => {
    onSave({ name: "John", age: 30 });
  };
  return <button onClick={handleClick}>Save</button>;
};

const ParentComponent: React.FC = () => {
  const handleSave = (data: { name: string }) => {
    console.log(data.name);
  };
  // <ChildComponent onSave={handleSave} /> // Error: Argument of type '{ name: string; }' is not assignable to parameter of type '{ name: string; age: number; }'. Property 'age' is missing.
  return <ChildComponent onSave={(data) => console.log(data)} />;
};

راه حل:

  • مطمئن شوید که تابع ارسال شده به Prop، دقیقاً همان پارامترها و نوع برگشتی را که در تعریف Prop مشخص شده است، می‌پذیرد.
  • اگر تابع شما به همه پارامترها نیاز ندارد، می‌توانید از آن‌ها چشم‌پوشی کنید یا آن‌ها را اختیاری کنید، اما نوع باید همچنان مطابقت داشته باشد.

با شناسایی و درک این خطاهای رایج، می‌توانید فرآیند دیباگینگ خود را سرعت بخشید و به طور موثرتری از TypeScript در توسعه React استفاده کنید.

نتیجه‌گیری: آینده توسعه Type-Safe با React و TypeScript

همانطور که در این مقاله جامع بررسی کردیم، ترکیب TypeScript با React نه تنها یک مزیت، بلکه یک ضرورت برای توسعه کامپوننت‌های مدرن، پایدار و قابل نگهداری در اکوسیستم فرانت‌اند است. از کاهش خطاهای زمان اجرا و بهبود چشمگیر تجربه توسعه‌دهنده گرفته تا افزایش مقیاس‌پذیری و همکاری تیمی، مزایای Type-safety غیرقابل انکار هستند.

ما گام به گام نحوه راه‌اندازی یک پروژه React-TypeScript را بررسی کردیم، به عمق تعریف Props و State برای کامپوننت‌های تابعی و کلاسی پرداختیم، و نحوه استفاده امن و Type-safe از Hooks محبوب React (مانند useState، useEffect، useRef و useContext) را آموختیم. همچنین با الگوهای پیشرفته TypeScript مانند کامپوننت‌های Polymorphic، Utility Types و نحوه تایپ کردن HOCs و Render Props آشنا شدیم که به ما امکان می‌دهند کامپوننت‌هایی انعطاف‌پذیرتر و قابل استفاده مجدد بسازیم.

پیروی از بهترین روش‌ها، مانند فعال کردن Strict Mode، اجتناب از any، استفاده هوشمندانه از Inference، و سازماندهی مناسب نوع‌ها، به شما کمک می‌کند تا از پتانسیل کامل TypeScript بهره‌مند شوید. در نهایت، درک و عیب‌یابی خطاهای رایج TypeScript، به شما این قدرت را می‌دهد که با اطمینان بیشتری کدنویسی کنید و چالش‌ها را به سرعت حل کنید.

با پیشرفت فناوری‌ها، تقاضا برای نرم‌افزارهای با کیفیت بالا و بدون خطا رو به افزایش است. TypeScript ابزاری قدرتمند است که به توسعه‌دهندگان React کمک می‌کند تا این انتظارات را برآورده کنند. سرمایه‌گذاری در یادگیری و پیاده‌سازی TypeScript در پروژه‌های React شما، نه تنها به نفع پروژه فعلی شما خواهد بود، بلکه مهارت‌های شما را به عنوان یک توسعه‌دهنده به طور قابل توجهی ارتقا خواهد داد و شما را برای چالش‌های آینده توسعه وب آماده می‌کند.

پس، با اطمینان قدم در مسیر توسعه Type-safe بگذارید و از ساخت کامپوننت‌هایی قدرتمندتر و پایدارتر با React و TypeScript لذت ببرید.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان