چرا تایپاسکریپت نمیتواند تشخیص دهد که یک متغیر پس از یک دستور if که برای undefined بررسی میکند، تعریف شده است، در حالی که never برمیگرداند
من در حال کامپایل در TypeScript 3.6.2 با strictNullChecks فعال هستم.
فرض کنید یک متغیر اعلام کردهام که ممکن است تعریف نشده باشد:
let filename: string|undefined;
سپس، یک تابع بازخوانی ممکن است مقداری به آن اختصاص دهد یا آن را تعریفنشده باقی بگذارد:
doIt(() => filename = "assigned");
اکنون بررسی می کنم که آیا پاسخ تماس به filename اختصاص داده شده است یا خیر. در غیر این صورت،نام فایل; در غیر این صورت،filename تعریف نشده است و من برنامه را خاتمه می دهم (مقدار بازگشتیهرگز):
if (filename === undefined) {
process.exit(0);
}
اگر این شرط if نادرست باشد، به این معنی است که filename باید یک مقدار رشتهای معتبر داشته باشد، درست است؟ در نهایت، من سعی میکنم از چیزی که به طور قطع یک رشته است استفاده کنم:
console.log(filename.toUpperCase());
با این حال، من یک خطا دریافت میکنم:
source/repro.ts:6:13 - error TS2532: Object is possibly 'undefined'.
6 console.log(filename.toUpperCase());
~~~~~~~~
Found 1 error.
تا جایی که من متوجه شدهام، به دلیل این که دستور if بالا یک بازگشت never دارد، این یعنی برنامه قبل از رسیدن به خطوط بعدی که از filename استفاده میکنند، پایان مییابد؛ بنابراین، filename باید یک رشته باشد! آیا چیزی را از قلم انداختهام؟ چرا TypeScript هنوز باور دارد که filename ممکن است بعد از بازگشت never مقدار undefined داشته باشد؟
برای بازتولید، در اینجا برنامه کامل است:
let filename: string|undefined;
doIt(() => filename = "assigned");
if (filename === undefined) {
process.exit(0);
}
console.log(filename.toUpperCase());
function doIt(fn: () => void) {
fn();
}
توجه: من میتوانم مشکل خود را در برنامه واقعی خود حل کنم زیرا میتوانم filename="" را مقداردهی اولیه کنم و در دستور if آن را بررسی کنم. با این حال، کنجکاوم بدانم چرا این روش خاص کار نخواهد کرد.
ویرایش: اینجا tsconfig.json من است. من این مثال را در یک پوشهی کاملاً جدید بدون tsconfig.json امتحان کردم و نتوانستم این خطا را بازتولید کنم. شاید چیزی در tsconfig من درست نباشد، اما هنوز آن را پیدا نکردهام:
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"declaration": true,
"alwaysStrict": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"noUnusedLocals": true
},
"include": [
"source/**/*.ts"
]
}
پاسخ شماره ۱
این رفتار در Typescript 3.6.3 و نسخههای قدیمیتر وجود دارد، اما در واقع در نسخه 3.7.2 همانطور که شما میخواهید کار میکند؛ اینجا یک لینک Playground برای مشاهده خودتان است. اگر بین نسخهها با استفاده از منو جابهجا شوید، خطا ظاهر و ناپدید میشود.
اگر این برای پروژه شما ضروری است، میتوانید تایپاسکریپت را ارتقا دهید.
به طور کلی، مشکل این بود که نمودار کنترل جریان (CFG) قبل از بررسی نوع تعیین میشود، بنابراین در زمان شکلگیری CFG (و بررسی دسترسی)، این واقعیت که exit مقدار never را برمیگرداند، در دسترس نیست، و بنابراین شاخه CFG که در آن exit فراخوانی میشود ادامه پیدا میکند به کدی که بعد از دستور if قرار دارد، جایی که متغیر در حالت ممکن است تعریفنشده قرار دارد.
این موضوع بهعنوان یک مسئله در GitHub در دسامبر ۲۰۱۶ مطرح شد، و طبق پاسخی در یک رشته مختلف،
#12825 عمومی کردن مدیریت نوع never برای بازگشتها
- گراف جریان کنترل در حین بایندینگ تشکیل میشود، اما هنوز دادههای نوع را نداریم
ما میتوانیم تمام تماسها را در هر نقطه کنترل جریان ذخیره کنیم و سپس آنها را برای عدم بازگشت بررسی کنیم و این اطلاعات را برای محاسبه نوعها بررسی کنیم
- گران!
تجزیه و تحلیل صحیح نیازمند چندین تکرار است
بنابراین اینها برخی از دلایلی هستند که ممکن است در نسخههای ۳.۶.۳ و قبلتر حل نشده باشد.
پاسخ شماره ۲
پس مسئله اصلی این است که در نسخه ۳.۶ توابعی که هرگز باز نمیگشتند، وارد تحلیل جریان کنترل نمیشدند. این ویژگی در نسخه ۳.۷ توسط این PR پیادهسازی شد.
اگر کد خود را اجرا کنید (با کپی کردن برخی از نوعها از تعاریف نود) میتوانیم ببینیم که در نسخه 3.7 کار میکند اما در نسخه 3.6 نه
همچنین تابع پیکان که متغیر را تنظیم میکند واقعاً هیچ تأثیری بر نتیجه ندارد. تایپاسکریپت هیچ تجزیه و تحلیل جریان کنترلی در مورد نحوه فراخوانی یک callback از doIt انجام نمیدهد. این موضوع در این مسئله به تفصیل توضیح داده شده است.
فقط با نسخه ۳.۷ مراقب باشید و هرگز توابع بازگشتی ایجاد نکنید، شرایط برای شرکت آنها در جریان کنترل بسیار سختگیرانه است:
یک فراخوانی تابع به عنوان فراخوانی تأکید یا فراخوانی که هرگز باز نمیگردد تجزیه و تحلیل میشود زمانی که
- این فراخوانی به عنوان یک عبارت سطح بالا رخ میدهد، و
- این فراخوانی یک شناسه واحد یا یک دنباله نقطهدار از شناسهها را برای نام تابع مشخص میکند، و
- هر شناسه در نام تابع به یک موجودیت با نوع صریح اشاره میکند، و
- نام تابع به نوع تابعی که دارای نوع بازگشتی با تأکید (asserts) یا یک حاشیهنویسی نوع بازگشتی صریح never است، حل میشود.
یک موجودیت زمانی که به عنوان یک تابع، متد، کلاس یا فضای نام، یا به عنوان یک متغیر، پارامتر یا ویژگی با یک حاشیهنویسی نوع صریح اعلام شود، دارای نوع صریح در نظر گرفته میشود. (این قانون خاص وجود دارد تا تحلیل جریان کنترل از فراخوانیهای احتمالی تأیید، تحلیل بیشتری را به صورت دورانی تحریک نکند.)
بنابراین عبارت تابع ممکن است در CFA شرکت نکند اگر حاوی توضیح صریح نباشد
const exit = () => {
throw new Error()
}
let filename: string | undefined;
if (filename === undefined) {
exit();
}
console.log(filename.toUpperCase()); // error
لینک زمین بازی با یک توضیح صریح کار میکند:
const exit: () => never = () => {
throw new Error()
}
let filename: string | undefined;
if (filename === undefined) {
exit();
}
console.log(filename.toUpperCase()); // error
لینک زمین بازی
