Kodokon kodokon.com

Narrowing: typeof, in, instanceof และ type guards

คุณจะเชี่ยวชาญกลไกการทำ type narrowing และเขียน predicate ของตัวเองเพื่อทำให้ union น่าเชื่อถือยิ่งขึ้น

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

Narrowing คือกลไกที่ TypeScript ใช้ปรับชนิดที่กว้าง (union หรือ unknown) ให้แม่นยำขึ้นด้วยการวิเคราะห์ control flow คุณใช้มันอยู่แล้วโดยไม่ทันคิด: ทุก if ที่ทำกับค่าที่มีชนิดจะกระตุ้นการวิเคราะห์ คอมไพเลอร์จะติดตาม guard ในตัวสามตระกูล: typeof สำหรับ primitive, instanceof สำหรับ instance ของคลาส และ in สำหรับการมีอยู่ของ property การเลือกใช้ให้ถูกต้องช่วยให้คุณไม่ต้อง cast แบบมั่ว ๆ ด้วย as ซึ่งเป็นการปิดการตรวจสอบแทนที่จะชี้นำมัน

TYPESCRIPT
function formatValue(
  value: string | number | Date
): string {
  if (typeof value === "string") {
    return value.trim();
  }
  if (value instanceof Date) {
    return value.toISOString();
  }
  return value.toFixed(2);
}
แต่ละสาขาจะ narrow ชนิดลง สาขาสุดท้ายจึงเป็น number ได้เท่านั้น

ตัวดำเนินการ in มีค่ามหาศาลเมื่อคุณจัดการอ็อบเจกต์ที่มีโครงสร้างต่างกันและไม่มีฟิลด์ตัวแบ่งแยก แต่ต้องระวัง: in ตรวจการมีอยู่ของคีย์ รวมถึงคีย์ที่สืบทอดมา ไม่ใช่ชนิดของมัน สำหรับข้อมูลภายนอก (API, JSON) มันจะ narrow ตามการประกาศของคุณ ไม่ใช่ตามสิ่งที่มีอยู่จริงตอน runtime กับดักคลาสสิกในโลกจริง: การตรวจ typeof value === "object" จะปล่อยให้ null ผ่านไปได้ เพราะ typeof null เป็น "object" ใน JavaScript ควรเพิ่มการตรวจ null อย่างชัดเจนเสมอ

TYPESCRIPT
interface Cat { meow(): void }
interface Dog { bark(): void }

function speak(animal: Cat | Dog): void {
  if ("meow" in animal) {
    animal.meow();
  } else {
    animal.bark();
  }
}
การ narrow ด้วยการมีอยู่ของ property โดยใช้ตัวดำเนินการ in

เมื่อตรรกะของ guard กลับมาใช้ซ้ำได้หรือซับซ้อนเกินกว่าการวิเคราะห์ flow จะรับมือได้ ให้เขียน custom type guard: ฟังก์ชันที่ชนิดค่าที่คืนถูก annotate ว่า value is T จากนั้นคอมไพเลอร์จะเชื่อในสิ่งที่คุณ implement มันเป็นสัญญา: ถ้าการตรวจของคุณไม่ครบถ้วน คุณก็นำ error ตอน runtime ที่ระบบชนิดจะไม่มีวันเห็นกลับเข้ามาอีกครั้ง ตั้งแต่ TypeScript 5.5 เป็นต้นมา predicate แบบง่าย ๆ จะถูก infer อัตโนมัติใน callback อย่าง filter แต่การ annotate อย่างชัดเจนยังเป็นแนวปฏิบัติหลักสำหรับฟังก์ชันที่ export ออกไป

TYPESCRIPT
interface User { id: string; email: string }

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "string" &&
    "email" in value &&
    typeof value.email === "string"
  );
}

const raw: unknown =
  JSON.parse('{"id":"1","email":"a@b.c"}');
if (isUser(raw)) {
  console.log(raw.email.toLowerCase());
}
predicate ที่ตรวจสอบข้อมูลภายนอกก่อนนำไปใช้

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ทำไมการตรวจ typeof value === "object" จึงไม่เพียงพอที่จะ narrow ให้เป็นอ็อบเจกต์ที่ไม่ใช่ null?
    • เพราะ typeof คืน "Object" โดยขึ้นต้นด้วยตัวพิมพ์ใหญ่
    • เพราะ typeof null ก็เป็น "object" ใน JavaScript เช่นกัน
    • เพราะ typeof ใช้กับ union ไม่ได้
    • เพราะ array คืน "array"
  2. annotation ของค่าที่คืนแบบใดที่เปลี่ยนฟังก์ชันให้เป็น custom type guard?
    • boolean
    • value as User
    • value is User
    • asserts value
  3. ความเสี่ยงหลักของ custom type guard ที่เขียนไม่ดีคืออะไร?
    • error ตอนคอมไพล์ภายในฟังก์ชัน
    • การ narrow ที่โกหก: คอมไพเลอร์เชื่อในชนิดที่ผิดตอน runtime
    • การสูญเสียประสิทธิภาพในการคอมไพล์