React עם TypeScript פירושו לכתוב קומפוננטות בקובצי .tsx שבהם ל-props, ל-state, ל-refs ול-event handlers יש טיפוסים, כך שהקומפיילר תופס prop חסר או event handler שגוי לפני שהקוד רץ. הטיפוסים נעלמים בזמן ה-build: מה שרץ בדפדפן הוא אותו JavaScript שהייתם כותבים בלעדיהם. העורכים החיים בדף הזה מריצים את ה-JavaScript הזה, והגרסה עם הטיפוסים יושבת ליד כל אחד בבלוק סטטי.
הנה אותו Badge עם טיפוסים. count אופציונלי (?), ולכן התג השני חוזר לברירת המחדל 0, ו-children מקבל גם אלמנט וגם מחרוזת רגילה.
type BadgeProps = {
label: string;
count?: number;
children: React.ReactNode;
};
function Badge({ label, count = 0, children }: BadgeProps) {
return (
<div>
<strong>{label}</strong> ({count})
<div>{children}</div>
</div>
);
}
<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'
יצירת פרויקט TypeScript
ל-Vite יש תבנית של React ועוד TypeScript:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
מקבלים קובצי .tsx, tsconfig.json עם strict דלוק, והגדרות הטיפוסים של React (@types/react, @types/react-dom) כבר מותקנות. Vite מסירה טיפוסים בזמן שהיא מגישה ובונה אבל לא בודקת אותם; סקריפט ה-build של התבנית מריץ קודם את tsc, והעורך שלכם בודק תוך כדי הקלדה. frameworks כמו Next.js מגדירים TypeScript באותה צורה כשיוצרים פרויקט עם ה-CLI שלהם.
טיפוסים ל-props
תארו את ה-props כטיפוס אובייקט ותנו טיפוס לפרמטר שעובר destructuring. כמה דפוסים מכסים את רוב הקומפוננטות:
interface UserCardProps {
name: string; // required
age?: number; // optional, may be undefined
role: 'admin' | 'member'; // a union of allowed strings
tags: string[];
onSelect: (id: string) => void; // a callback prop
children?: React.ReactNode; // anything React can render
}
function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
// ...
}
type או interface. שניהם עובדים ל-props. interface אפשר להרחיב עם extends ולמזג על ידי הצהרה עליו פעמיים; type יכול גם לבטא unions, intersections ו-mapped types. רוב הצוותים בוחרים אחד ומשתמשים בו בכל מקום.
children. השתמשו ב-React.ReactNode לכל מה שנכנס בין התגיות: אלמנטים, מחרוזות, מספרים, מערכים, null. הדף על props מסביר איך children מגיע מלכתחילה.
למה React.FC אופציונלי. const Badge: React.FC<BadgeProps> = (...) => ... נותן טיפוס לכל הפונקציה במקום לפרמטר. מאז הטיפוסים של React 18 הוא כבר לא מוסיף children בשבילכם, הוא לא יכול לקבל פרמטר טיפוס בשביל קומפוננטה גנרית, והוא לא מוסיף שום דבר שפרמטר עם טיפוס לא נותן. קוד ישן משתמש בו הרבה; פונקציות רגילות עם props מוטפסים הן הבחירה הנפוצה לקוד חדש.
טיפוסים ל-useState
לערך התחלתי פשוט, ההסקה מספיקה: useState(0) הוא number, useState('') הוא string. העבירו ארגומנט טיפוס כשה-state יכול להחזיק יותר ממה שהערך ההתחלתי מרמז: ערך שמתחיל כ-null, או אחת מכמה מחרוזות קבועות.
לחצו על "Load user" והסטטוס עובר מ-idle ל-loading ל-done. ה-state עם הטיפוסים:
type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');
user.name; // Error: 'user' is possibly 'null'
user?.name; // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'
בלי <User | null>, useState(null) מסיק את הטיפוס null ו-setUser(data) נכשל בקימפול. ה-union מכריח אתכם לטפל במקרה הריק, וזו בדיוק הבדיקה user ? ... : ... בדוגמה הרצה.
useRef לאלמנטי DOM
ref שמצביע על אלמנט DOM מקבל את טיפוס האלמנט ומתחיל כ-null, כי האלמנט לא קיים עד ש-React מחברת אותו אחרי הרינדור הראשון.
הקלידו משהו, ואז לחצו על "Log the value": הקונסול מדפיס את מה שהקלדתם, שנקרא ישר מאלמנט ה-DOM. הגרסה עם הטיפוסים:
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null
inputRef.current?.focus(); // OK
inputRef.current.focus(); // Error: 'inputRef.current' is possibly 'null'
const timerId = useRef<number | null>(null); // a mutable value, not a DOM node
השתמשו ב-interface של האלמנט שתואם לתגית: HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. בטיפוסים של React 19 useRef תמיד מקבל ארגומנט ומחזיר RefObject שאפשר להציב ב-current שלו, כך שאותה צורה עובדת ל-refs של DOM ולערכים כמו מזהה טיימר. ראו useRef לצד של זמן הריצה.
טיפוסים ל-event handlers
handlers שכתובים inline לא צריכים טיפוס: ב-onChange={(e) => setName(e.target.value)} TypeScript מכיר את e מה-prop שאליו הוא מועבר. תנו טיפוס כשמעבירים handler לפונקציה משלו.
שלחו והקונסול רושם את הכתובת; preventDefault מונע מהדף להיטען מחדש. עם טיפוסים:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}
function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') { /* ... */ }
}
מאז @types/react 19.2.10, onSubmit מקבל את הטיפוס React.SubmitEvent וה-React.FormEvent הישן מסומן כמיושן; עם גרסה ישנה יותר של הטיפוסים, כתבו במקום React.FormEvent<HTMLFormElement>. פרמטר הטיפוס הוא האלמנט שה-handler מחובר אליו. אם אתם לא בטוחים בטיפוס, רחפו מעל ה-prop onChange בעורך שלכם, או תנו טיפוס לכל ה-handler בבת אחת: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.
טיפוסים לקונטקסט
תנו ל-createContext את טיפוס הערך. אם יש ברירת מחדל הגיונית, העבירו אותה, וכל צרכן מקבל ערך שאינו null:
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
const theme = useContext(ThemeContext); // Theme
כשאין ברירת מחדל טובה (משתמש מחובר, store), התחילו ב-null ועטפו את useContext בהוק שזורק שגיאה אם קומפוננטה משמשת מחוץ ל-provider. כל מי שקורא לו מקבל אז את הטיפוס שאינו null והודעת שגיאה ברורה במקום קריסה עמוק בפנים.
type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);
export function useAuth(): Auth {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
return value;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState('Ada');
return (
<AuthContext value={{ user, logout: () => setUser('') }}>
{children}
</AuthContext>
);
}
<AuthContext value={...}> הוא תחביר ה-provider של React 19; <AuthContext.Provider value={...}> מקבל טיפוסים באותה צורה.
קומפוננטות גנריות
רשימה, טבלה או select שעובדים עם כל טיפוס פריט הם קומפוננטה גנרית: פרמטר טיפוס מחבר את ה-prop items ל-callback renderItem.
ה-List עם הטיפוסים:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
getKey: (item: T) => string | number;
};
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error
TypeScript מסיק את T מ-items, כך שמי שקורא לקומפוננטה אף פעם לא כותב את פרמטר הטיפוס. בקובץ .tsx פונקציית חץ צריכה <T,> במקום <T>, כי <T> לבד נקרא כתגית JSX.
הרחבת props של אלמנטים נייטיב
עטיפה סביב button או input צריכה לקבל כל מאפיין שהאלמנט הנייטיב מקבל. React.ComponentProps<'button'> הוא הסט המלא הזה; הוסיפו מעליו props משלכם ועשו spread לשאר.
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'ghost';
};
function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
return (
<button
{...rest}
style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
/>
);
}
<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
Save
</Button>;
onClick, disabled, type ו-aria-* כולם מקבלים טיפוס בלי לפרט אותם. ב-React 19 ref הוא prop רגיל, ו-ComponentProps<'button'> כולל אותו, כך ש-<Button ref={buttonRef}> מגיע לצומת ה-DOM דרך ה-spread בלי forwardRef. השתמשו ב-ComponentPropsWithoutRef<'button'> כשרוצים להשאיר את ref בחוץ, וב-ComponentProps<typeof UserCard> כדי לקרוא את ה-props של אחת הקומפוננטות שלכם.
שגיאות טיפוסים נפוצות
"'ref.current' is possibly 'null'". האלמנט לא קיים בזמן הרינדור הראשון. השתמשו ב-ref.current?.focus(), או בדקו if (ref.current) לפני השימוש.
"Type 'string' is not assignable" על state מסוג union. const [status, setStatus] = useState('idle') מסיק string, שרחב יותר ממה שרוצים, והעברה שלו ל-prop עם הטיפוס Status נכשלת. כתבו useState<Status>('idle').
אירוע עם טיפוס של האלמנט הלא נכון. React.ChangeEvent<HTMLInputElement> על select נותן ל-e.target את הטיפוס הלא נכון. פרמטר הטיפוס חייב להתאים לאלמנט שה-handler מחובר אליו.
שימוש ב-any כדי להשתיק שגיאה. זה מכבה בדיקה לכל מה שהערך הזה נוגע בו. העדיפו unknown וצמצמו אותו, או תקנו את הטיפוס במקור שלו.
שאלות נפוצות
איך יוצרים אפליקציית React עם TypeScript?
הריצו npm create vite@latest my-app -- --template react-ts, ואז npm install ו-npm run dev. קומפוננטות נכתבות בקובצי .tsx ו-Vite מסירה את הטיפוסים בזמן ה-build; הריצו tsc (סקריפט ה-build של התבנית עושה את זה) כדי לבדוק אותם.
מה הטיפוס של children ב-React?
React.ReactNode. הוא מקבל כל דבר ש-React יודעת לרנדר: אלמנטים, מחרוזות, מספרים, מערכים, null ו-undefined. השתמשו ב-React.ReactElement רק כשצריך בדיוק אלמנט אחד.
איך נותנים טיפוס ל-useState עם null?
העבירו את הטיפוס במפורש כ-union: useState<User | null>(null). בלי ארגומנט הטיפוס TypeScript מסיק ש-null הוא הערך היחיד המותר.
מה הטיפוס של אירוע onChange ב-React?
React.ChangeEvent<HTMLInputElement> לשדה input (HTMLTextAreaElement או HTMLSelectElement לאלמנטים האלה). אז e.target.value מקבל את הטיפוס string.
כדאי להשתמש ב-React.FC?
זה אופציונלי. מתן טיפוס ישירות לפרמטר ה-props (function Card({ title }: CardProps)) עושה את אותה עבודה, נקרא פשוט יותר ועובד עם גנריים. React.FC כבר לא מוסיף children במובלע, כך שהוא נותן מעט מאוד.
להשתמש ב-type או ב-interface בשביל props?
שניהם עובדים. interface אפשר להרחיב ולמזג; type יכול לבטא unions ו-mapped types. בחרו מוסכמה אחת לבסיס הקוד והיצמדו אליה.