وب مدرن، رابط کاربری و تجربهی وب · فصل ۳ — Type System، Runtime، Async و Build · 41
TypeScript Strict: Type System برای کد قابل اعتماد
میتواند «TypeScript Strict: Type System برای کد قابل اعتماد» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خلاصهٔ درس
میتواند «TypeScript Strict: Type System برای کد قابل اعتماد» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خروجی این درس
میتواند «TypeScript Strict: Type System برای کد قابل اعتماد» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
پیشنیازها
یک feature میتواند در TypeScript بدون خطای کامپایل جدی به نظر برسد، اما هنوز در مرز واقعی داده ناامن باشد. اگر داده از JSON، storage، پیام، API یا هر منبع runtime دیگری آمده باشد، یک interface بهتنهایی شکل واقعی آن داده را بررسی نمیکند. این درس دربارهٔ همین فاصله است: TypeScript چه چیزی را هنگام کامپایل میتواند بررسی کند، چه چیزی فقط در زمان اجرا معلوم میشود، و برای یک ادعای محدود دقیقاً چه شاهدی لازم است.
در پایان این درس، یک mapper کوچک را تعمیر میکنی و Strict Type Boundary Evidence Record v1 میسازی. این رکورد باید جداگانه نشان دهد:
- کدام خطا با تنظیمات strict در مرحلهٔ کامپایل دیده شد؛
- مرز نامطمئن را کجا بهصورت
unknownنگه داشتی؛ - کدام runtime guard واقعاً شکل داده را بررسی کرد؛
- چه mutationی یک اطمینان کاذب ایجاد کرد و چگونه آن را تشخیص دادی؛
- نسخهٔ compiler و تنظیمات مؤثر چه بودهاند؛
- و حتی پس از repair چه محدودیتی هنوز باقی است.
این درس نمیگوید که strict صحت runtime را تضمین میکند، type annotation دادهٔ بیرونی را validate میکند، هر any یا هر assertion همیشه اشتباه است، یا کامپایل تمیز معادل رفتار درست کسبوکار است.
مسئلهٔ واقعی: قرارداد تایپی با دادهٔ واقعی یکی نیست
فرض کن یک fixture محلی JSON داریم:
{
"userId": "u-104",
"retries": 2,
"note": "retry after cache reset"
}
و این مدل را نوشتهای:
export interface RequestPayload {
userId: string;
retries: number;
note?: string | null;
}
وجود این interface فقط یک قرارداد TypeScript برای کد ماست. اگر JSON واقعی این باشد:
{
"userId": "u-104",
"retries": "two"
}
خود interface رشتهٔ "two" را در runtime به عدد تبدیل یا رد نمیکند. اگر دادهٔ نامطمئن را با assertion مستقیماً به RequestPayload تبدیل کنیم، ممکن است compiler ساکت شود، در حالی که مرز اعتماد هنوز بررسی نشده است.
مدل ذهنی این درس
مسیر قابل دفاع چنین است:
دادهٔ نامطمئن → unknown → بررسی runtime → narrowing → مسیر typed → شاهد کامپایل + شاهد runtime
هر پیکان یک ادعای متفاوت دارد. اگر این مراحل را با هم مخلوط کنی، خیلی زود به جملهٔ خطرناک «TypeScript قبولش کرد، پس داده معتبر است» میرسی.
پل ترجمهٔ JavaScript/TypeScript — اگر syntax معمولی وب هنوز خودکار نشده است
این bridge قابل رد کردن است. اگر قطعههای کوچک JavaScript/TypeScript را میخوانی و میتوانی const، object/array، function، if، loop، import و throw را توضیح بدهی، مستقیم به بخش «strict یک کلید جادویی برای runtime نیست» برو. هدف این قسمت آموزش کامل JavaScript نیست؛ فقط مفاهیم برنامهنویسی قبلی را به vocabulary این درس ترجمه میکند تا موضوع اصلی واقعاً strict typing و trust boundary باشد.
نام و value: از binding به const و let
const retries: number = 2;
let status: string = "queued";
status = "running";
const یعنی خود binding دوباره به value دیگری assign نمیشود؛ let اجازه reassignment میدهد. annotationهای : number و : string به TypeScript کمک میکنند دربارهٔ استفادهٔ statically typed از آن نام reasoning کند. این annotationها دادهٔ runtime را validate نمیکنند.
Primitiveهای پرکاربرد این درس string، number و boolean هستند:
const userId: string = "u-104";
const retries: number = 2;
const completed: boolean = false;
record/list به object/array تبدیل میشود
مدلی مثل record/list را در TypeScript معمولاً با object/array میبینی:
const task = { id: 1, title: "A", completed: false };
const tasks = [task, { id: 2, title: "B", completed: true }];
task.completed یک field از object را میخواند و tasks[0] یک عضو array را. interface بعداً یک type contract برای shape مورد انتظار میدهد؛ object واقعی را در runtime بازرسی نمیکند.
function، parameter و return همان نقش قبلی را دارند
function isOpen(task: { completed: boolean }): boolean {
return task.completed === false;
}
task parameter است، : boolean بعد از parameter shape/type آن را محدود میکند و : boolean بعد از پرانتز نوع return را بیان میکند. خود return نتیجه را به caller میدهد.
boolean، comparison و if
if (task.completed === false) {
// this branch runs only when the comparison is true
}
=== comparison میسازد؛ if بر اساس boolean branch را انتخاب میکند. وقتی بعداً narrowing میبینی، TypeScript از همین control flow برای محدودترکردن type ممکن استفاده میکند.
loop و map
دو الگوی عادی برای پیمایش array:
for (const task of tasks) {
// inspect each task
}
const ids = tasks.map((task) => task.id);
for...of اعضا را یکییکی میبیند. map از هر عضو یک value تازه میسازد و array نتیجه را برمیگرداند. اینها runtime behavior هستند؛ type system فقط دربارهٔ استفادهٔ کد از valueها reasoning میکند.
import/export یعنی نامها را بین moduleها جابهجا کنیم
export function mapRequest(input: unknown) { /* ... */ }
import { mapRequest } from "./mapper";
export نام را برای moduleهای دیگر قابل استفاده میکند و import آن را وارد module فعلی میکند. جزئیات bundler/module-resolution موضوع این bridge نیست.
JSON.parse یک boundary runtime است، نه validator
const decoded = JSON.parse(text);
این command متن JSON را به یک value جاوااسکریپت تبدیل میکند. اما اینکه parse موفق شده، فقط syntax JSON را تأیید میکند؛ ثابت نمیکند value شکل RequestPayload را دارد. برای همین در درس اصلی value نامطمئن را در مرز تحت مالکیت خود به unknown نگه میداریم و بعد runtime evidence میسازیم.
throw مسیر خطا را متوقف و آشکار میکند
if (!isRequestPayload(input)) {
throw new TypeError("Invalid request payload");
}
اگر شرط نامعتبر بودن true شود، throw اجرای عادی آن مسیر را متوقف میکند و error را به caller/runtime میسپارد. وجود throw بهتنهایی ثابت نمیکند guard درست یا کامل است؛ test باید behavior را مشاهده کند.
micro-readiness check — آیا میتوانی bridge را ببندی؟
قبل از ادامه، بدون حفظکردن syntax جواب بده:
- فرق
constوletدر binding چیست؟ - در
const tasks = [...]، array چیست و object داخل آن چیست؟ - در یک function کوچک parameter و return را پیدا کن.
- توضیح بده
ifوfor...ofچه control-flowی میسازند. - بگو چرا
JSON.parseموفق، اثبات shape/domain نیست و چرا ممکن است پس از آنunknownنگه داریم. - بگو
throwچه اتفاقی در مسیر اجرا ایجاد میکند.
اگر این شش مورد روشن است، bridge بسته است. از اینجا به بعد موضوع جدید درس TypeScript strictness، unknown، runtime guard و narrowing است؛ نه syntax پایه. اگر یکی مبهم است فقط همان بند را دوباره بخوان، سپس وارد مدل اصلی شو.
1. strict یک کلید جادویی برای runtime نیست
گزینهٔ strict مجموعهای از بررسیهای سختگیرانهتر TypeScript را فعال میکند. جزئیات این خانواده به نسخه و پیکربندی compiler وابسته است؛ بنابراین یک evidence record فنی باید نسخهٔ compiler و configuration مؤثر را ثبت کند. این درس از strict: true استفاده میکند، اما نتیجهای که میگیریم فقط دربارهٔ همان محیط ثبتشده و همان feature محدود است.
یکی از مهمترین پیامدهای strict checks در این fixture، دیدن دسترسی ناامن به مقدار nullable است. اگر note اختیاری یا null باشد، نوشتن این کد بدون guard قابل دفاع نیست:
const normalized = request.note.trim();
Compiler میتواند در اینجا مشکل تایپی را نشان دهد. اما این هنوز چیزی دربارهٔ JSON نامعتبر پیش از ساخت request ثابت نمیکند.
2. مرز اعتماد را با unknown آشکار کن
وقتی شکل runtime یک مقدار را هنوز بررسی نکردهای، در این lesson آن را در مرز خودت به unknown نگه میداری:
const decoded: unknown = JSON.parse(text);
نکتهٔ مهم این نیست که JSON.parse ذاتاً برای تو validation انجام میدهد؛ نمیدهد. نکته این است که تو اجازه نمیدهی نوع مطمئنِ دامنه قبل از شاهد runtime وارد مسیر شود.
در مقابل، این کد فقط سکوت compiler را میخرد:
const decoded = JSON.parse(text) as RequestPayload;
as RequestPayload یک runtime validator نیست. assertion به compiler میگوید «این ادعا را بپذیر»، اما شیء واقعی را با contract مقایسه نمیکند.
3. Narrowing باید بر یک مشاهدهٔ runtime تکیه کند
برای این feature، یک guard کوچک کافی است:
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isRequestPayload(value: unknown): value is RequestPayload {
if (!isRecord(value)) return false;
const note = value.note;
return (
typeof value.userId === "string" &&
typeof value.retries === "number" &&
(note === undefined || note === null || typeof note === "string")
);
}
وقتی guard true برمیگرداند، TypeScript میتواند در branch بعدی نوع دقیقتری برای value در نظر بگیرد. این narrowing است. شاهد آن دو لایه دارد:
- runtime check واقعاً روی value اجرا شده است؛
- compiler بعد از آن check اجازهٔ استفادهٔ typed را میدهد.
اگر فقط لایهٔ دوم را با assertion تقلید کنی، بخش اول از بین رفته است.
Compile-time و runtime را در یک جدول قاطی نکن
| پرسش | شاهد اصلی | چیزی که ثابت نمیکند |
|---|---|---|
| آیا strict یک nullable access ناامن را میگیرد؟ | خروجی compiler در همان نسخه/config | شکل واقعی دادهٔ بیرونی معتبر است |
| آیا ورودی شکل مورد انتظار را دارد؟ | runtime guard روی value واقعی | همهٔ قواعد کسبوکار درستاند |
| آیا بعد از guard مسیر typed قابل استفاده است؟ | narrowing + compile evidence | guard تمام semantic constraintهای محصول را پوشش میدهد |
| آیا malformed input بهدرستی رد میشود؟ | behavior test با fixture نامعتبر | تمام inputهای ممکن پوشش داده شدهاند |
| آیا repair در زمینهٔ دیگری هم قابل دفاع است؟ | changed-context implementation + tests | این یک بار انتقال، mastery کلی را ثابت میکند |
مثال کارشده: mapper شکننده
نسخهٔ اولیه چنین است:
export function mapRequest(input: unknown): RequestPayload {
const candidate = input as RequestPayload;
return {
userId: candidate.userId.trim(),
retries: candidate.retries,
note: candidate.note.trim()
};
}
دو مشکل متفاوت داریم:
candidate.note.trim()باstrictNullChecksیک مشکل compile-time دارد، چونnoteمیتواند absent یاnullباشد.- assertion باعث شده
userIdوretriesبدون هیچ runtime evidence وارد مسیر trusted شوند. حتی اگر nullable branch را درست کنی، assertion هنوز validation نیست.
Repair باید این دو failure mode را جدا حل کند. نسخهٔ قابل دفاع ابتدا guard را اجرا میکند، سپس nullable field را صریح مدیریت میکند:
export function mapRequest(input: unknown): RequestPayload {
if (!isRequestPayload(input)) {
throw new TypeError("Invalid request payload");
}
return {
userId: input.userId.trim(),
retries: input.retries,
note: input.note == null ? null : input.note.trim()
};
}
این repair هنوز ادعا نمیکند retries حتماً مثبت است یا userId قواعد کسبوکار خاصی را رعایت میکند؛ چنین constraintهایی در contract این lesson تعریف نشدهاند.
سه mutation که باید بتوانی توضیح بدهی
Mutation A — guard را با assertion عوض کن
پیشبینی: ممکن است compile سبز بماند، اما malformed input دیگر در trust boundary رد نمیشود و behavior test باید regression را آشکار کند.
Mutation B — nullable branch را ضعیف کن
پیشبینی: اگر دوباره مستقیماً input.note.trim() را بنویسی، strict compile باید دسترسی ناامن را بگیرد.
Mutation C — فقط مرز تحت مالکیت خودت را از unknown به any صریح تبدیل کن
پیشبینی: ممکن است tsc همچنان pass شود و اگر runtime guard را نگه داشته باشی، malformed-input testها هم همچنان سبز بمانند. همین نکته هدف probe است: دستور node tests/check_boundary.cjs src/mapper.ts را اجرا کن. static boundary check قابلمشاهده برای learner باید signature دارای any صریح را رد کند. سه مشاهده را جدا ثبت کن—نتیجهٔ compile، نتیجهٔ static boundary check و نتیجهٔ runtime—و failureای را که واقعاً رخ نداده جعل نکن.
این تفاوت مهم است: قرار نیست همهٔ mutationها یک نوع failure بسازند. اگر oracle تو فقط «compiler باید fail شود» باشد، compile/runtime split را هنوز نفهمیدهای.
آزمایشگاه: Strict Type Boundary Evidence Record v1
در پوشهٔ آزمایشگاه یک starter عمداً ناقص وجود دارد. ترتیب کار:
- نسخهٔ TypeScript compiler را ثبت کن (
tsc --version). tsconfig.jsonرا بررسی کن و تأیید کنstrict: trueفعال است.- baseline compile را اجرا کن و failure مربوط به nullable assumption را ثبت کن.
- قبل از تغییر کد، نتیجهٔ سه mutation بالا را پیشبینی کن.
- assertion را با runtime guard جایگزین کن و nullable field را صریح مدیریت کن.
- compile را دوباره اجرا کن.
- behavior tests را روی fixture معتبر و malformed اجرا کن.
- mutationها را اجرا کن و برای هرکدام بنویس: prediction → observation → diagnosis → repair.
- یک residual limitation ثبت کن؛ مثلاً guard این lesson فقط shape/typeهای انتخابشده را بررسی میکند و قواعد دامنهای دیگری را اثبات نمیکند.
Evidence Record باید این فیلدها را داشته باشد
- compiler version و strict configuration؛
- owned claim؛
- compile-time observation؛
- runtime observation؛
- mutation result؛
- repair result؛
- changed-context result؛
- residual limitation.
سه نوع پشتیبانی، یک evidence bar
اگر Foundation هستی، vocabulary و baseline probe را مرحلهبهمرحله دنبال کن. اگر Repair هستی، از failure واقعی شروع کن و فقط همان misconception را بازسازی کن. اگر Experienced هستی، میتوانی syntax walkthrough را فشرده کنی. هیچکدام از این مسیرهای پشتیبانی اجازه نمیدهند mutation، runtime evidence یا residual limitation حذف شود.
تمرین انتقال — زمینه عوض میشود، قرارداد نه
اکنون یک fixture متفاوت را در نظر بگیر: تنظیمات صفحه از JSON محلی decode میشود و باید به این مدل برسد:
interface PageSettings {
theme: "light" | "dark";
layout: { columns: number; compact: boolean };
tags: string[];
}
این shape عمداً با RequestPayload فرق دارد: باید یک literal union، یک رکورد nested و تکتک اعضای آرایه را validate کنی، نه اینکه فقط سه فیلد flat را rename کنی. بدون کپیکردن guard قبلی، یک boundary design مستقل بساز. خروجی انتقال باید شامل این موارد باشد:
- trust boundary کجاست و چرا مقدار را ابتدا نامطمئن میدانی؛
- runtime evidence چه چیزی را بررسی میکند؛
- narrowing کجا رخ میدهد؛
- compile-time evidence چه چیزی را میگیرد؛
- یک malformed input و نتیجهٔ behavior test؛
- یک claim boundary؛
- یک update condition: چه تغییر در schema/config/compiler باعث میشود evidence را دوباره بررسی کنی.
موفقیت در این انتقال شاهد مفیدی است که روش را فقط حفظ نکردهای، اما بهتنهایی mastery کلی TypeScript را ثابت نمیکند.
مرز ادعا
پس از این درس میتوانی دربارهٔ یک feature کوچک بگویی:
من trust boundary را صریح کردهام، compile-time و runtime evidence را جدا ثبت کردهام، حداقل یک failure mutation را دیده و repair کردهام، و محدودیت باقیمانده را میدانم.
اما هنوز نمیتوانی فقط با تکیه بر این درس بگویی:
- این برنامه در runtime بهطور کلی صحیح است؛
- تمام inputهای بیرونی validate شدهاند؛
- همهٔ استفادههای
anyیا assertion اشتباهاند؛ - یا compile سبز، behavior درست محصول را ثابت میکند.
منابع و تازگی
این درس سه claim فنی را به مستندات رسمی TypeScript محدود میکند: خانوادهٔ بررسیهای strict، جدایی TypeScript از رفتار runtime جاوااسکریپت و حذف type information در خروجی، و narrowing مبتنی بر runtime/control-flow evidence. چون رفتار strict میتواند با نسخههای TypeScript سختگیرانهتر شود، compiler version بخشی از evidence است و پس از تغییر نسخه/config باید evidence مربوط دوباره اجرا شود.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
Q1 retrieval
Q2 application
Q3 diagnosis
Q4 decision
Q5 transfer
ملاکهای ارزیابی (rubric)
| Dimension | Critical | Sufficient evidence | Blocking failure |
|---|---|---|---|
| Contract correctness | Yes | The repair preserves the bounded RequestPayload contract and keeps uncertain input untrusted until runtime evidence exists. | Treats an assertion/interface as runtime validation or silently expands the lesson into unowned business rules. |
| Compile-time evidence | Yes | Records compiler version/config and shows which static/nullability problem the compiler catches. | Claims a compile pass proves runtime validity. |
| Runtime evidence | Yes | Runs valid and malformed fixtures against an actual runtime guard and records observed behavior. | No malformed-input check or fabricated runtime evidence. |
| Mutation diagnosis | Yes | Uses a distinct oracle for each mutation; for unknown→any it records compile result, the learner-visible static boundary check, and runtime result separately, without requiring a fabricated runtime failure. | Requires every mutation to fail compilation, omits the explicit-any static boundary oracle, or cannot distinguish compile/runtime/static-boundary observations. |
| Scope discipline | Yes | States at least one residual limitation and avoids universal anti-any/anti-assertion claims. | Overclaims whole-program correctness or runtime validation. |
| Transfer independence | Yes | Changed PageSettings task reconstructs the method for a materially different shape using a literal union, nested object, and string array; includes trust boundary, runtime evidence, narrowing, compile evidence, malformed behavior, claim boundary and update condition without decisive scaffold. | Transfer omits claim boundary/update condition or can be solved as a mechanical property rename of the first guard. |
AI support boundary
AI may suggest candidate checks after the learner states the claim and evidence need. It may not fabricate compiler/runtime observations, provide the decisive changed-context implementation, or replace the learner's residual-limit judgment.
منابع و تازگی
مطالعهٔ موارد (case studies)
تصاویر درس
اسکریپت ویدیوی اختیاری
نمای آغازین: تصویر ۱ — مسیر «داده نامطمئن → unknown → runtime guard → narrowing → typed path → compile + runtime evidence».
یک interface به دادهٔ runtime نگاه نمیکند. این ویدیو فقط یک سؤال را دنبال میکند: کجا حق داریم یک value نامطمئن را بهعنوان domain type استفاده کنیم؟
ابتدا starter را ببین: assertion باعث میشود compiler دربارهٔ shape داده سؤال نکند، و nullable field هم یک failure جدا دارد. سپس guard را اضافه میکنیم. وقتی check واقعی انجام شد، narrowing معنا پیدا میکند.
در mutation اول، guard را با assertion عوض میکنیم. compile ممکن است سبز بماند، اما malformed input باید regression را نشان دهد. در mutation دوم، nullable branch را خراب میکنیم و compiler باید مشکل static را بگیرد. در mutation سوم، boundary را any میکنیم تا ببینیم چرا «compile سبز» بهتنهایی evidence کافی نیست.
جمعبندی: یک evidence record خوب نسخه compiler، compile observation، runtime observation، mutation، repair و محدودیت باقیمانده را جدا ثبت میکند.
فایلهای آزمایشگاه
این fixture solution آماده ندارد و عمداً rename همشکلِ RequestPayload نیست: theme یک literal union است، layout یک رکورد nested دارد و tags آرایهای از stringهاست. mapper.ts را مستقل پیاده کن و بعد bash run_checks.sh را اجرا کن.
Evidence انتقال باید این قرارداد را نگه دارد:
- trust boundary و دلیل نامطمئنبودن مقدار اولیه؛
- runtime check برای literal union، فیلدهای nested layout و تکتک اعضای tags؛
- narrowing پیش از استفادهٔ typed؛
- compile-time evidence؛
- رفتار malformed input؛
- یک claim boundary؛
- یک update condition که مشخص کند چه تغییر schema/config/compiler باعث میشود evidence را دوباره اجرا کنی.
Guard مربوط به RequestPayload را خطبهخط کپی نکن؛ روش را برای shape و control-flow متفاوت بازسازی کن.
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)