TypeScript در React قرارداد Props، State و Eventها را روشن میکند و بخشی از ناسازگاریها را پیش از اجرا نشان میدهد. این بررسی به معنی اطمینان کامل از رفتار برنامه نیست. پاسخ شبکه ممکن است نامعتبر باشد، کاربر مجوز نداشته باشد یا تابعی با نوع درست نتیجه تجاری غلط بدهد. هدف، بیان رابطههای مشخص در کد است تا استفاده نادرست زودتر دیده شود و همکار بعدی شکل مورد انتظار داده را بفهمد.
برای مثال فرضی یک فرم یادداشت، وضعیت ارسال میتواند بیکار، در حال ارسال، موفق یا خطا باشد. اگر چند Boolean مستقل برای این حالتها بگذارید، ترکیبهای متناقض ممکن میشوند؛ هم موفق و هم خطا یا هم بیکار و هم در حال ارسال. Union با فیلد متمایزکننده میتواند حالتهای مجاز را محدود کند. هنوز باید انتقال حالتها را درست بنویسید. TypeScript به تنهایی تشخیص نمیدهد Server واقعاً درخواست را دریافت کرده است یا نه.
Props را با نیاز مصرفکننده تعریف کنید
راهنمای TypeScript در React تعریف Props و استفاده نوعها با Hookها را توضیح میدهد. برای بسیاری از Function Componentها، پارامتر دارای type یا interface کافی است. انتخاب میان این دو با نیاز تعریف و قرارداد تیم انجام شود؛ یکی به طور عمومی حرفهایتر از دیگری نیست. React.FC نیز ابزار ممکن است، اما استفاده از آن شرط صحت Component نیست. قرارداد باید روشن کند کدام مقدار ضروری و کدام اختیاری است.
Optional بودن را با Null یکی نکنید. نبود یک Prop، مقدار null و رشته خالی میتوانند معنی متفاوتی داشته باشند. برای children، نوع متناسب با محتوای مجاز را انتخاب کنید؛ اگر هر محتوای قابل Render پذیرفته میشود، ReactNode معمولاً از فرض یک عنصر واحد روشنتر است. برای دکمه یا input مشترک، استفاده از نوع Props عنصر و محدود کردن بخشهای لازم میتواند تکرار را کم کند. اما حذف یک Prop استاندارد باید دلیل داشته باشد، چون ممکن است قابلیت دسترسی مصرفکننده را محدود کند.
State را با حالتهای واقعی مدل کنید
useState در مقدار اولیه ساده بسیاری از نوعها را استنباط میکند. Generic صریح وقتی مفید است که مقدار اولیه همه حالتهای مجاز را نشان نمیدهد، مثل State دارای null یا Union. انتخاب نوع بسیار باز برای جلوگیری از خطای Compiler، رابطه را ضعیف میکند. به جای اینکه همه فیلدها اختیاری شوند، حالتهای مستقل را جدا کنید. این کار در JSX هم کمک میکند فقط در شاخه مناسب به مقدار خطا یا نتیجه دسترسی داشته باشید.
Union باید با رفتار محصول هماهنگ باشد. در فرم، failure به پیام نیاز دارد و success به شناسه یا نتیجه دریافتشده. اگر ارسال لغو شد، تصمیم بگیرید به idle برمیگردد یا حالت جدا دارد. مدل نوع نباید تعداد حالتهایی را که رابط نیاز دارد پنهان کند. مقاله فرم مرحلهای درباره تفاوت پیشنویس و ثبت نهایی مفید است. دو وضعیت با ظاهر مشابه ممکن است تعهد متفاوتی نسبت به کار کاربر داشته باشند.
Event را از عنصر واقعی نوعدهی کنید
برای Handler مستقل، نوع Event را با عنصر هماهنگ کنید؛ تغییر input با Submit فرم یک نوع نیست. currentTarget به عنصری اشاره دارد که Handler به آن وصل شده و در بسیاری از کاربردها برای خواندن مقدار از target مناسبتر است. Handler inline معمولاً از JSX استنباط میشود. استخراج آن به تابع جدا نباید نوع را به any تبدیل کند. این بررسی مشخص میکند کدام property قابل استفاده است، اما اعتبار تجاری مقدار واردشده هنوز باید بررسی شود.
عدد موجود در input ابتدا ممکن است رشته باشد. تبدیل با Number به تنهایی شرط دامنه را اجرا نمیکند؛ رشته خالی، NaN و مقدار خارج از حد باید با انتظار فرم بررسی شوند. برای متن هم trim یا محدودیت طول باید با Server هماهنگ باشد. نوع string نمیگوید متن غیرخالی یا ایمیل معتبر است. مقاله طراحی فرم درباره زمان راهنما و پیام خطا توضیح دارد. نوع مناسب و پیام قابل اقدام دو بخش مکمل یک قرارداد هستند.
داده شبکه را با Assertion معتبر نکنید
نوشتن as ResponseType کنار داده JSON، بررسی Runtime ایجاد نمیکند. داده را به عنوان unknown بگیرید و با Parser یا Schema معتبر بررسی کنید. پس از آن، DTO مناسب رابط ساخته شود. مقاله TypeScript پیشرفته نمونه کامل همین مرز را نشان میدهد. Contract نوعی فقط درباره کدی است که Compiler بررسی میکند؛ Server دیگر، فایل و ورودی کاربر ممکن است با آن قرارداد هماهنگ نباشند و باید در مرز ورود بررسی شوند.
خطاهای Parser را با شکست شبکه و خطای تجاری جدا کنید. ممکن است درخواست از نظر HTTP موفق باشد اما شکل پاسخ مطابق انتظار نباشد. نمایش یک نتیجه ناقص به جای گزارش محدودیت، تجربه نامطمئن میسازد. مدل State باید اجازه دهد این وضعیت با پیام مناسب مدیریت شود. جزئیات داخلی Parser لازم نیست به کاربر نمایش داده شود، ولی تیم باید مسیر پیگیری داشته باشد. ذخیره متن خام حساس در Log نیز صرفاً برای تشخیص نوع توصیه عمومی مناسبی نیست.
نمونه کامل React و TypeScript
چهار فایل زیر پروژه آموزشی مستقلی میسازند. Node.js سازگار با Vite 7 و npm لازماند؛ در این مسیر شاخه 20.19 یا 22.12 به بالا در محدوده پشتیبانی آن ابزار قرار میگیرد. package.json، tsconfig.json و index.html در ریشه و main.tsx در پوشه src قرار میگیرند. npm install و npm run dev را در همان ریشه اجرا کنید. فرمان npm run build ابتدا tsc را برای بررسی نوع فراخوانی میکند و سپس Vite خروجی میسازد؛ صرف اجرای Dev Server جای بررسی نوع نیست.
{"name":"typed-react-demo","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"tsc --noEmit && vite build"},"dependencies":{"react":"^19.0.0","react-dom":"^19.0.0"},"devDependencies":{"typescript":"^5.0.0","@types/react":"^19.0.0","@types/react-dom":"^19.0.0","vite":"^7.0.0"}}
{"compilerOptions":{"target":"ES2022","lib":["ES2022","DOM"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"noEmit":true,"skipLibCheck":true},"include":["src"]}
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>فرم TypeScript</title><style>body{font:1rem/1.8 Tahoma,sans-serif;max-width:40rem;margin:2rem auto;padding:1rem}input,button{font:inherit;padding:.5rem}input:focus-visible,button:focus-visible{outline:3px solid #111;outline-offset:3px}</style></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
در کد اصلی، NoteState یک Union است. فرم فقط متن غیرخالی تا دویست کاراکتر را میپذیرد و در زمان ارسال، input و دکمه موقتاً غیرفعال میشوند. saveDemo یک عملیات آموزشی با تأخیر کوتاه است و شبکه یا ذخیره واقعی ندارد. اگر متن «خطا» وارد شود، شاخه خطا نمایش داده میشود. این شرط برای مشاهده حالت رابط است و رفتار یک API واقعی یا قانون محصول نیست. Dependencyهای لازم در package مشخصاند و فایل پنهان دیگری وجود ندارد.
import {useState,type FormEvent} from 'react';
import {createRoot} from 'react-dom/client';
type NoteState={status:'idle'}|{status:'sending'}|{status:'success';receipt:Readonly<{text:string}>}|{status:'failure';message:string};
async function saveDemo(text:string):Promise<Readonly<{text:string}>>{await new Promise<void>(resolve=>setTimeout(resolve,300));if(text==='خطا')throw new Error('خطای آموزشی؛ ذخیره واقعی انجام نمیشود.');return {text}}
function App(){const [text,setText]=useState('');const [state,setState]=useState<NoteState>({status:'idle'});
async function submit(event:FormEvent<HTMLFormElement>){event.preventDefault();const value=text.trim();if(!value||value.length>200){setState({status:'failure',message:'متن غیرخالی تا ۲۰۰ کاراکتر لازم است.'});return}setState({status:'sending'});try{const receipt=await saveDemo(value);setState({status:'success',receipt})}catch(error:unknown){setState({status:'failure',message:error instanceof Error?error.message:'خطای نامشخص نمونه'})}}
return <main><h1>یادداشت آموزشی</h1><p>داده به سرور ارسال نمیشود؛ متن «خطا» حالت شکست نمونه را نشان میدهد.</p><form onSubmit={submit}><label htmlFor="note">متن یادداشت</label><input id="note" value={text} maxLength={200} disabled={state.status==='sending'} onChange={event=>setText(event.currentTarget.value)}/><button disabled={state.status==='sending'}>اجرای نمونه</button></form><p role="status">{state.status==='sending'?'در حال اجرای نمونه':state.status==='success'?`نتیجه محلی: ${state.receipt.text}`:state.status==='failure'?state.message:'آماده'}</p></main>}
createRoot(document.getElementById('root')!).render(<App/>);
Handler ارسال از FormEvent فرم استفاده میکند و preventDefault از رفتار معمول انتقال صفحه جلوگیری میکند. State موفق فقط در شاخه success به receipt دسترسی دارد و State خطا فقط در شاخه failure به message. مقدار اولیه idle همه حالتها را نشان نمیدهد، بنابراین Generic صریح نوشته شده است. readonly در قرارداد نمونه، قصد تغییر ندادن آن داده از مصرفکننده را بیان میکند و object را در Runtime Freeze نمیکند. این مرز را در توضیح رفتار حفظ کنید.
Generic و Utility Type باید رابطه مفید بسازند
Generic Component وقتی مفید است که رابطه میان آیتم و Callback را حفظ کند؛ مثلاً انتخاب ردیف همان نوع آیتم را به مصرفکننده بدهد. اگر همه مقدارها به unknown یا any برمیگردند، Generic ممکن است فقط ظاهر پیچیده بسازد. Pick و Omit برای انتخاب بخش قرارداد مناسباند، اما Partial کردن همه دادهها نباید نبود فیلد ضروری را پنهان کند. DTO و مدل پایگاه داده لزوماً با چند Utility Type یکسان نمیشوند؛ تفاوت مسئولیتها را صریح نگه دارید.
برای Componentهای دارای حالت، Props ناسازگار را با Union تعریف کنید. دکمه Link با href و دکمه عملیاتی با onClick ممکن است قرارداد متفاوت داشته باشند. این کار باید با HTML واقعی و دسترسیپذیری هماهنگ باشد؛ Union خوب، عنصر اشتباه را درست نمیکند. HOC یا Hook هم نباید بررسی مجوز Server را حذف کند. مخفی کردن دکمه در React یک تصمیم نمایشی است و اجازه انجام عملیات باید در مرز معتبر Backend بررسی شود.
بازبینی را با رفتار و Build تمام کنید
strict بودن تنظیمات میتواند ناسازگاریهای بیشتری را آشکار کند، اما باید با مسیر Build و توسعه تیم هماهنگ باشد. استثناها را با دلیل مشخص نگه دارید و خطا را با Assertion بیدلیل خاموش نکنید. تغییر نسخه React و نوعهای آن ممکن است قرارداد بعضی APIها را تغییر دهد؛ راهنمای رسمی همان نسخه را ببینید. مقاله الگوهای Component درباره مرز مسئولیت و عملکرد React درباره هزینه تعامل زمینه بیشتری دارند.
در تحویل، نوع Props، حالتهای State، مرز داده بیرونی و روش بررسی نوع را ثبت کنید. نمونه آموزشی بدون اجرای واقعی Server نباید به عنوان پیادهسازی ثبت پایدار معرفی شود. تصمیم عملی این است که از یک فرم یا Component مشخص شروع کنید: قرارداد ورودی را محدود، حالتهای معتبر را جدا و نتیجه بیرونی را بررسی کنید. TypeScript زمانی به نگهداری کمک میکند که نوعها درباره رفتار مشخص حرف بزنند و تغییر محصول همراه تغییر همان قرارداد باشد.
هنوز دیدگاهی ثبت نشده
اولین نفری باشید که نظر میدهد!