ค้นหาเว็บไซต์

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 ไปยังโค้ด