ไทย

สำรวจ type guards และ type assertions ใน TypeScript เพื่อเพิ่มความปลอดภัยของไทป์ (type safety) ป้องกันข้อผิดพลาดขณะรันไทม์ และเขียนโค้ดที่แข็งแกร่งและบำรุงรักษาง่ายขึ้น เรียนรู้พร้อมตัวอย่างและแนวทางปฏิบัติที่ดีที่สุด

เชี่ยวชาญด้าน Type Safety: คู่มือฉบับสมบูรณ์เกี่ยวกับ Type Guards และ Type Assertions

ในโลกของการพัฒนาซอฟต์แวร์ โดยเฉพาะเมื่อทำงานกับภาษาที่เป็น dynamic typing เช่น JavaScript การรักษาความปลอดภัยของไทป์ (type safety) อาจเป็นความท้าทายที่สำคัญ TypeScript ซึ่งเป็น superset ของ JavaScript เข้ามาจัดการปัญหานี้โดยการนำเสนอ static typing อย่างไรก็ตาม แม้จะมีระบบไทป์ของ TypeScript ก็ยังมีสถานการณ์ที่คอมไพเลอร์ต้องการความช่วยเหลือในการอนุมานไทป์ที่ถูกต้องของตัวแปร นี่คือจุดที่ type guards และ type assertions เข้ามามีบทบาท คู่มือฉบับสมบูรณ์นี้จะเจาะลึกฟีเจอร์ที่ทรงพลังเหล่านี้ พร้อมนำเสนอตัวอย่างที่นำไปใช้ได้จริงและแนวทางปฏิบัติที่ดีที่สุดเพื่อเพิ่มความน่าเชื่อถือและความสามารถในการบำรุงรักษาโค้ดของคุณ

Type Guards คืออะไร?

Type guards คือนิพจน์ (expression) ของ TypeScript ที่ช่วยจำกัดขอบเขตไทป์ของตัวแปรให้แคบลงภายในขอบเขตที่กำหนด ช่วยให้คอมไพเลอร์เข้าใจไทป์ของตัวแปรได้แม่นยำกว่าที่อนุมานไว้ในตอนแรก ซึ่งมีประโยชน์อย่างยิ่งเมื่อต้องจัดการกับ union types หรือเมื่อไทป์ของตัวแปรขึ้นอยู่กับเงื่อนไขขณะรันไทม์ การใช้ type guards จะช่วยให้คุณหลีกเลี่ยงข้อผิดพลาดขณะรันไทม์และเขียนโค้ดที่แข็งแกร่งยิ่งขึ้น

เทคนิค Type Guard ทั่วไป

TypeScript มีกลไกในตัวหลายอย่างสำหรับสร้าง type guards:

การใช้ typeof

typeof operator เป็นวิธีที่ตรงไปตรงมาในการตรวจสอบไทป์พื้นฐาน (primitive type) ของตัวแปร โดยจะคืนค่าเป็นสตริงที่ระบุไทป์นั้นๆ

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // TypeScript รู้ว่า 'value' เป็น string ที่นี่
  } else {
    console.log(value.toFixed(2)); // TypeScript รู้ว่า 'value' เป็น number ที่นี่
  }
}

printValue("hello"); // ผลลัพธ์: HELLO
printValue(3.14159); // ผลลัพธ์: 3.14

การใช้ instanceof

instanceof operator ใช้ตรวจสอบว่าอ็อบเจกต์เป็นอินสแตนซ์ของคลาสใดคลาสหนึ่งหรือไม่ ซึ่งมีประโยชน์อย่างยิ่งเมื่อทำงานกับการสืบทอด (inheritance)

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof!");
  }
}

function makeSound(animal: Animal) {
  if (animal instanceof Dog) {
    animal.bark(); // TypeScript รู้ว่า 'animal' เป็น Dog ที่นี่
  } else {
    console.log("Generic animal sound");
  }
}

const myDog = new Dog("Buddy");
const myAnimal = new Animal("Generic Animal");

makeSound(myDog); // ผลลัพธ์: Woof!
makeSound(myAnimal); // ผลลัพธ์: Generic animal sound

การใช้ in

in operator ใช้ตรวจสอบว่าอ็อบเจกต์มี property ที่ระบุหรือไม่ ซึ่งมีประโยชน์เมื่อต้องจัดการกับอ็อบเจกต์ที่อาจมี property แตกต่างกันไปตามไทป์ของมัน

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function move(animal: Bird | Fish) {
  if ("fly" in animal) {
    animal.fly(); // TypeScript รู้ว่า 'animal' เป็น Bird ที่นี่
  } else {
    animal.swim(); // TypeScript รู้ว่า 'animal' เป็น Fish ที่นี่
  }
}

const myBird: Bird = { fly: () => console.log("Flying"), layEggs: () => console.log("Laying eggs") };
const myFish: Fish = { swim: () => console.log("Swimming"), layEggs: () => console.log("Laying eggs") };

move(myBird); // ผลลัพธ์: Flying
move(myFish); // ผลลัพธ์: Swimming

Custom Type Guard Functions

สำหรับสถานการณ์ที่ซับซ้อนยิ่งขึ้น คุณสามารถกำหนดฟังก์ชัน type guard ของคุณเองได้ ฟังก์ชันเหล่านี้จะคืนค่าเป็น type predicate ซึ่งเป็นนิพจน์บูลีนที่ TypeScript ใช้เพื่อจำกัดขอบเขตไทป์ของตัวแปรให้แคบลง โดย type predicate จะอยู่ในรูปแบบ variable is Type

interface Square {
  kind: "square";
  size: number;
}

interface Circle {
  kind: "circle";
  radius: number;
}

type Shape = Square | Circle;

function isSquare(shape: Shape): shape is Square {
  return shape.kind === "square";
}

function getArea(shape: Shape) {
  if (isSquare(shape)) {
    return shape.size * shape.size; // TypeScript รู้ว่า 'shape' เป็น Square ที่นี่
  } else {
    return Math.PI * shape.radius * shape.radius; // TypeScript รู้ว่า 'shape' เป็น Circle ที่นี่
  }
}

const mySquare: Square = { kind: "square", size: 5 };
const myCircle: Circle = { kind: "circle", radius: 3 };

console.log(getArea(mySquare)); // ผลลัพธ์: 25
console.log(getArea(myCircle)); // ผลลัพธ์: 28.274333882308138

Type Assertions คืออะไร?

Type assertions คือวิธีการบอกคอมไพเลอร์ของ TypeScript ว่าคุณรู้เกี่ยวกับไทป์ของตัวแปรมากกว่าที่มันเข้าใจในปัจจุบัน เป็นวิธีการลบล้างการอนุมานไทป์ของ TypeScript และระบุไทป์ของค่าอย่างชัดเจน อย่างไรก็ตาม สิ่งสำคัญคือต้องใช้ type assertions ด้วยความระมัดระวัง เนื่องจากอาจข้ามการตรวจสอบไทป์ของ TypeScript และอาจนำไปสู่ข้อผิดพลาดขณะรันไทม์ได้หากใช้อย่างไม่ถูกต้อง

Type assertions มีสองรูปแบบ:

โดยทั่วไปแล้วคีย์เวิร์ด as จะเป็นที่นิยมมากกว่าเนื่องจากเข้ากันได้ดีกับ JSX

เมื่อใดที่ควรใช้ Type Assertions

โดยทั่วไปแล้ว Type assertions จะใช้ในสถานการณ์ต่อไปนี้:

ตัวอย่างของ Type Assertions

การทำ Type Assertion อย่างชัดเจน

ในตัวอย่างนี้ เรายืนยันว่าการเรียก document.getElementById จะคืนค่าเป็น HTMLCanvasElement หากไม่มีการยืนยันไทป์ TypeScript จะอนุมานเป็นไทป์ที่ทั่วไปกว่าคือ HTMLElement | null

const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d"); // TypeScript รู้ว่า 'canvas' เป็น HTMLCanvasElement ที่นี่

if (ctx) {
  ctx.fillStyle = "#FF0000";
  ctx.fillRect(0, 0, 150, 75);
}

การทำงานกับไทป์ Unknown

เมื่อทำงานกับข้อมูลจากแหล่งภายนอก เช่น API คุณอาจได้รับข้อมูลที่มีไทป์เป็น unknown คุณสามารถใช้ type assertion เพื่อบอก TypeScript ว่าจะจัดการกับข้อมูลนั้นอย่างไร

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  const data = await response.json();
  return data as User; // ยืนยันว่าข้อมูลนี้เป็น User
}

fetchUser(1)
  .then(user => {
    console.log(user.name); // TypeScript รู้ว่า 'user' เป็น User ที่นี่
  })
  .catch(error => {
    console.error("Error fetching user:", error);
  });

ข้อควรระวังในการใช้ Type Assertions

ควรใช้ Type assertions เท่าที่จำเป็นและด้วยความระมัดระวัง การใช้ type assertions มากเกินไปสามารถบดบังข้อผิดพลาดของไทป์พื้นฐานและนำไปสู่ปัญหาขณะรันไทม์ได้ นี่คือข้อควรพิจารณาที่สำคัญบางประการ:

Type Narrowing

Type guards มีความเชื่อมโยงอย่างใกล้ชิดกับแนวคิดของ type narrowing ซึ่งเป็นกระบวนการปรับปรุงไทป์ของตัวแปรให้เป็นไทป์ที่เฉพาะเจาะจงมากขึ้นโดยอิงตามเงื่อนไขหรือการตรวจสอบขณะรันไทม์ Type guards คือเครื่องมือที่เราใช้เพื่อให้เกิด type narrowing

TypeScript ใช้การวิเคราะห์ control flow เพื่อทำความเข้าใจว่าไทป์ของตัวแปรเปลี่ยนแปลงไปอย่างไรในแต่ละสาขาของโค้ด เมื่อมีการใช้ type guard, TypeScript จะอัปเดตความเข้าใจภายในเกี่ยวกับไทป์ของตัวแปรนั้น ทำให้คุณสามารถใช้เมธอดและ property ที่เฉพาะเจาะจงสำหรับไทป์นั้นได้อย่างปลอดภัย

ตัวอย่างของ Type Narrowing

function processValue(value: string | number | null) {
  if (value === null) {
    console.log("Value is null");
  } else if (typeof value === "string") {
    console.log(value.toUpperCase()); // TypeScript รู้ว่า 'value' เป็น string ที่นี่
  } else {
    console.log(value.toFixed(2)); // TypeScript รู้ว่า 'value' เป็น number ที่นี่
  }
}

processValue("test"); // ผลลัพธ์: TEST
processValue(123.456); // ผลลัพธ์: 123.46
processValue(null); // ผลลัพธ์: Value is null

แนวทางปฏิบัติที่ดีที่สุด (Best Practices)

เพื่อใช้ประโยชน์จาก type guards และ type assertions ในโปรเจกต์ TypeScript ของคุณอย่างมีประสิทธิภาพ ควรพิจารณาแนวทางปฏิบัติที่ดีที่สุดต่อไปนี้:

ข้อควรพิจารณาในระดับนานาชาติ

เมื่อพัฒนาแอปพลิเคชันสำหรับผู้ใช้ทั่วโลก โปรดคำนึงว่า type guards และ type assertions สามารถส่งผลกระทบต่อการปรับให้เข้ากับท้องถิ่น (localization) และการทำให้เป็นสากล (internationalization หรือ i18n) ได้อย่างไร โดยเฉพาะอย่างยิ่ง ให้พิจารณาถึง:

บทสรุป

Type guards และ type assertions เป็นเครื่องมือสำคัญในการเพิ่มความปลอดภัยของไทป์และเขียนโค้ด TypeScript ที่แข็งแกร่งยิ่งขึ้น ด้วยการทำความเข้าใจวิธีใช้ฟีเจอร์เหล่านี้อย่างมีประสิทธิภาพ คุณสามารถป้องกันข้อผิดพลาดขณะรันไทม์ ปรับปรุงความสามารถในการบำรุงรักษาโค้ด และสร้างแอปพลิเคชันที่น่าเชื่อถือมากขึ้น อย่าลืมเลือกใช้ type guards มากกว่า type assertions เมื่อใดก็ตามที่เป็นไปได้ จัดทำเอกสารสำหรับ type assertions ของคุณ และตรวจสอบข้อมูลจากภายนอกเพื่อให้แน่ใจว่าข้อมูลไทป์ของคุณถูกต้อง การนำหลักการเหล่านี้ไปใช้จะช่วยให้คุณสร้างซอฟต์แวร์ที่มีเสถียรภาพและคาดการณ์ได้มากขึ้น เหมาะสำหรับการนำไปใช้งานทั่วโลก

เชี่ยวชาญด้าน Type Safety: คู่มือฉบับสมบูรณ์เกี่ยวกับ Type Guards และ Type Assertions | MLOG