typescript - การยืนยันค่าของ null หรือ undefined ไม่ถูกตรวจจับในฟังก์ชันซ้อน
ฉันมีฟังก์ชันนี้:
function assertNonNullish<TValue>(
value: TValue,
error?: Error
): asserts value is NonNullable<TValue> {
if (value === null || value === undefined) {
error ??= new Error("Something does not exist");
throw error;
}
}
นี่ควรจะตรวจสอบว่าบางสิ่งไม่ได้เป็นค่า null หรือ undefined หากฉันใช้ฟังก์ชันนี้ typescript ก็จะรู้ด้วยว่าบางสิ่งไม่ได้เป็นค่า null หรือ undefined
อย่างไรก็ตาม เมื่อฉันใช้ฟังก์ชันนี้ในฟังก์ชันอื่นแบบนี้:
function has_email() {
const email = localStorage.getItem('email')
assertNonNullish(email, new Error("email does not exist"));
}
และจากนั้นใช้ฟังก์ชัน has_email เพื่อตรวจสอบอาร์กิวเมนต์ของฟังก์ชันอีกตัว:
function print_email(email: string) {
console.log(email);
}
has_email() // Throws if email is not available but ts doesn't pick up on this
const email = localStorage.getItem('email')
print_email(email)
ดูเหมือนว่า TypeScript จะไม่สามารถระบุได้ว่าการรัน has_email() จะเหมือนกับ assertNonNullish(localStorage.getItem('email'))
มีวิธีใดที่จะให้ TypeScript รู้ไหม? ผม/ดิฉันอยากทำแบบนี้เพราะว่าผม/ดิฉันต้องตรวจสอบสถานะที่ต้องถูกโหลดจาก indexDB และผม/ดิฉันต้องรอให้โหลดเสร็จก่อน
คำตอบ #1
น่าเสียดายที่ตัวคอมไพเลอร์ TypeScript ไม่สามารถติดตามสิ่งที่เกิดขึ้นในโค้ดของคุณได้ดีพอที่จะให้ประเภทข้อมูลที่คุณคาดหวัง โค้ดตัวอย่างของคุณกำลังเรียกใช้งานสองครั้งในรูปแบบ
const email1 = localStorage.getItem('email');
const email2 = localStorage.getItem('email');
และคุณคาดหวังว่าค่าของ email1 และ email2 จะเหมือนกัน ดังนั้นพวกมันควรมีประเภทเดียวกัน (เช่น หาก email1 ไม่เป็น nullish ก็หมายความว่า email2 ก็ไม่เป็นเช่นกัน) แต่ TypeScript ไม่มีวิธีที่ดีในการเข้ารหัสความคาดหวังดังกล่าว การตรวจสอบประเภทของ email1 จะไม่ทำให้คอมไพเลอร์สามารถสรุปสิ่งใดเกี่ยวกับพฤติกรรมต่อไปของ localStorage.getItem() ได้เลย และจริง ๆ แล้วฉันก็ไม่แน่ใจว่าคุณควรสรุปอะไรมากนักด้วย หากการเรียกทั้งสองเกิดขึ้นในเธรดเดียวกันและไม่มีการเปลี่ยนแปลงข้อมูลเก็บระหว่างนั้น คุณอาจจะบอกได้ว่าการเรียกครั้งที่สองจะคืนค่าเหมือนกับครั้งแรก แต่โดยทั่วไปแล้วไม่มีการรับประกันเช่นนั้น
เรามาแก้ปัญหานั้นโดยการมีการเรียกใช้ localStorage.getItem('email') เพียงครั้งเดียว และตัวแปร const เพียงตัวเดียวชื่อ email ซึ่งค่าของมันจะเหมือนเดิมเสมอไม่ว่าคุณจะตรวจสอบเมื่อใด:
const email: string | null = localStorage.getItem('email');
function has_email() {
assertNonNullish(email, new Error("email does not exist"));
}
has_email();
ตอนนี้มั่นใจได้ 100% ว่าโค้ดใด ๆ ที่รันหลังจาก has_email() จะสามารถเข้าถึงตัวแปร email ที่ไม่เป็นค่า null ได้ แต่คอมไพเลอร์ยังไม่เห็นมัน:
email.toUpperCase(); // error! Object is possibly null
ทำไมจะไม่ล่ะ?
นี่เป็นข้อจำกัดโดยทั่วไปของ TypeScript ตามที่อธิบายใน microsoft/TypeScript#9998 เมื่อคุณตรวจสอบหรือยืนยันชนิดของค่าบางค่าเพื่อจำกัดชนิดที่ปรากฏของมัน ตัวคอมไพเลอร์จะใช้การวิเคราะห์การไหลของการควบคุมเพื่อเก็บข้อมูลว่าการจำกัดชนิดนั้นควรใช้ที่ใดและเมื่อใด แต่การวิเคราะห์นี้ทำงานได้เฉพาะภายในขอบเขตของฟังก์ชันเดียวเท่านั้น มันจะไม่ข้ามขอบเขตของฟังก์ชัน ซึ่งหมายความว่าการจำกัดชนิดใดๆ ภายในฟังก์ชันจะไม่สามารถเข้าถึงจากภายนอก และในทางกลับกัน ในตัวอย่างเฉพาะของเรา การจำกัดชนิดของ email ที่เกิดขึ้นภายในร่างของ has_email() จะไม่มีผลต่อโค้ดภายนอก และเราจะได้รับข้อผิดพลาด
ข้อจำกัดนี้ไม่สามารถแก้ไขได้ง่ายๆ ในฐานะมนุษย์ ฉันสามารถนึกภาพการรันโค้ดข้างต้นสองครั้ง ครั้งหนึ่งสมมติว่า ตัวแปร email ที่มีชนิด string | null เป็น string และอีกครั้งหนึ่งสมมติว่ามันเป็น null ในกรณีแรก ทุกอย่างเป็นไปด้วยดี และในกรณีหลัง คุณจะไม่เจอบรรทัดที่มีปัญหา สำหรับการรวมของสมาชิกสองตัวและฟังก์ชัน has_email() ที่ถูกเรียกครั้งเดียว นี่สามารถทำได้ง่าย แต่สิ่งนี้ไม่สามารถปรับขนาดได้ ฟังก์ชันส่วนใหญ่ถูกเรียกหลายครั้ง และถ้าคุณมีตัวแปรประเภท union หลายตัว คุณจะต้องจำลองทุกการรวมค่าที่เป็นไปได้สำหรับการเรียกฟังก์ชันแต่ละครั้ง ฉันเคยแนะนำที่ microsoft/TypeScript#25051 ว่าคุณสามารถขอพฤติกรรมนี้แบบเลือกเข้าร่วมได้ (เช่น "กรุณาทำการวิเคราะห์ประเภทนี้เฉพาะสำหรับตัวแปร email"), แต่ถูกปฏิเสธ; สิ่งแบบนี้ต้องเกิดขึ้นโดยอัตโนมัติเท่านั้นหรือไม่เกิดเลย
นอกจากนี้ยังไม่มีแนวคิดของฟังก์ชันยืนยันหรือพรีดิเคตประเภทที่ค่าที่คุณกำลังจำกัดประเภทไม่ใช่หนึ่งในพารามิเตอร์ของฟังก์ชัน (หรือบริบท this ของเมธอด) คุณไม่สามารถใส่คำอธิบาย has_email(): asserts email is string ได้ เนื่องจาก email ไม่ใช่หนึ่งในพารามิเตอร์ของ has_email() ดังนั้นอีกครั้ง ไม่มีวิธีใดที่จะบอกคอมไพเลอร์ว่า has_email() มีผลกระทบใด ๆ ต่อสถานะของ email
ด้วยเหตุผลเหล่านี้ คุณจึงจำเป็นต้องหาวิธีแก้ไขมัน
คุณไม่สามารถรักษาอัลกอริทึมของคุณให้เหมือนเดิมโดยไม่สูญเสียความปลอดภัยของประเภทบางอย่าง คุณสามารถระงับข้อผิดพลาดด้วยการยืนยันประเภทได้:
// assertion
email!.toUpperCase(); // okay
แต่นั่นจะกดข้อผิดพลาดแม้ว่าคุณจะลืมเรียก has_email() ก่อนก็ตาม
หรือถ้าคุณให้ความสำคัญกับความมั่นใจในความปลอดภัยของประเภทข้อมูลมากกว่ากับอัลกอริทึมเฉพาะของคุณ คุณสามารถทำการปรับปรุงโค้ดได้ ปัจจุบันคุณกำลังกำหนดค่า email ในที่หนึ่ง แล้วตรวจสอบด้วย has_email() ในอีกที่หนึ่ง ทำให้เกิดความเป็นไปได้ว่าคุณอาจเข้าถึง email เมื่อมันอาจเป็น null แทนที่จะทำเช่นนั้น เราสามารถเขียนเวอร์ชันที่การกำหนดค่าและการป้องกันประเภทเกิดขึ้นพร้อมกัน:
function getDefiniteEmail(): string {
const email = localStorage.getItem('email');
assertNonNullish(email);
return email;
}
const email = getDefiniteEmail();
email.toUpperCase(); // okay
ตอนนี้เมื่อคุณเรียกใช้ getDefiniteEmail() คุณจะรู้ว่าคุณจะได้รับ string กลับมาและไม่ใช่ null; มันถูกตรวจสอบแล้ว! นี่ช่วยเลี่ยงปัญหาทั้งหมดของการวิเคราะห์การไหลของควบคุมที่ข้ามขอบเขตฟังก์ชัน เพราะเราได้เข้ารหัสสถานะที่เราสนใจในประเภทการคืนค่าของ getDefiniteEmail() แล้ว.
ลิงก์ Playground ไปยังโค้ด
