Tiếng Việt

Khám phá type guards và type assertions trong TypeScript để tăng cường tính an toàn kiểu, ngăn ngừa lỗi runtime và viết mã mạnh mẽ, dễ bảo trì hơn. Học với các ví dụ thực tế và thực tiễn tốt nhất.

Làm chủ tính an toàn kiểu: Hướng dẫn toàn diện về Type Guards và Type Assertions

Trong lĩnh vực phát triển phần mềm, đặc biệt khi làm việc với các ngôn ngữ kiểu động như JavaScript, việc duy trì tính an toàn kiểu có thể là một thách thức đáng kể. TypeScript, một superset của JavaScript, giải quyết mối lo ngại này bằng cách giới thiệu kiểu tĩnh. Tuy nhiên, ngay cả với hệ thống kiểu của TypeScript, các tình huống phát sinh khi trình biên dịch cần hỗ trợ trong việc suy luận kiểu chính xác của một biến. Đây là lúc type guardstype assertions phát huy tác dụng. Hướng dẫn toàn diện này sẽ đi sâu vào các tính năng mạnh mẽ này, cung cấp các ví dụ thực tế và các phương pháp hay nhất để tăng cường độ tin cậy và khả năng bảo trì của mã của bạn.

Type Guards là gì?

Type guards là các biểu thức TypeScript thu hẹp kiểu của một biến trong một phạm vi cụ thể. Chúng cho phép trình biên dịch hiểu kiểu của một biến chính xác hơn so với suy luận ban đầu. Điều này đặc biệt hữu ích khi xử lý các union types hoặc khi kiểu của một biến phụ thuộc vào các điều kiện runtime. Bằng cách sử dụng type guards, bạn có thể tránh các lỗi runtime và viết mã mạnh mẽ hơn.

Các kỹ thuật Type Guard phổ biến

TypeScript cung cấp một số cơ chế tích hợp để tạo type guards:

Sử dụng typeof

typeof operator là một cách đơn giản để kiểm tra kiểu nguyên thủy của một biến. Nó trả về một chuỗi cho biết kiểu.

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // TypeScript biết 'value' là một chuỗi ở đây
  } else {
    console.log(value.toFixed(2)); // TypeScript biết 'value' là một số ở đây
  }
}

printValue("hello"); // Output: HELLO
printValue(3.14159); // Output: 3.14

Sử dụng instanceof

instanceof operator kiểm tra xem một đối tượng có phải là một instance của một lớp cụ thể hay không. Điều này đặc biệt hữu ích khi làm việc với kế thừa.

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 biết 'animal' là một Dog ở đây
  } else {
    console.log("Generic animal sound");
  }
}

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

makeSound(myDog); // Output: Woof!
makeSound(myAnimal); // Output: Generic animal sound

Sử dụng in

in operator kiểm tra xem một đối tượng có một thuộc tính cụ thể hay không. Điều này hữu ích khi xử lý các đối tượng có thể có các thuộc tính khác nhau tùy thuộc vào kiểu của chúng.

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

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

function move(animal: Bird | Fish) {
  if ("fly" in animal) {
    animal.fly(); // TypeScript biết 'animal' là một Bird ở đây
  } else {
    animal.swim(); // TypeScript biết 'animal' là một Fish ở đây
  }
}

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); // Output: Flying
move(myFish); // Output: Swimming

Custom Type Guard Functions

Đối với các trường hợp phức tạp hơn, bạn có thể định nghĩa các hàm type guard của riêng mình. Các hàm này trả về một type predicate, là một biểu thức boolean mà TypeScript sử dụng để thu hẹp kiểu của một biến. Một type predicate có dạng 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 biết 'shape' là một Square ở đây
  } else {
    return Math.PI * shape.radius * shape.radius; // TypeScript biết 'shape' là một Circle ở đây
  }
}

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

console.log(getArea(mySquare)); // Output: 25
console.log(getArea(myCircle)); // Output: 28.274333882308138

Type Assertions là gì?

Type assertions là một cách để cho trình biên dịch TypeScript biết rằng bạn biết nhiều hơn về kiểu của một biến so với những gì nó hiện hiểu. Chúng là một cách để ghi đè suy luận kiểu của TypeScript và chỉ định rõ ràng kiểu của một giá trị. Tuy nhiên, điều quan trọng là phải sử dụng type assertions một cách thận trọng, vì chúng có thể bỏ qua việc kiểm tra kiểu của TypeScript và có khả năng dẫn đến các lỗi runtime nếu sử dụng không chính xác.

Type assertions có hai dạng:

as keyword thường được ưu tiên hơn vì nó tương thích hơn với JSX.

Khi nào nên sử dụng Type Assertions

Type assertions thường được sử dụng trong các trường hợp sau:

Ví dụ về Type Assertions

Explicit Type Assertion

Trong ví dụ này, chúng ta khẳng định rằng lệnh gọi document.getElementById sẽ trả về một HTMLCanvasElement. Nếu không có khẳng định, TypeScript sẽ suy ra một kiểu chung chung hơn là HTMLElement | null.

const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d"); // TypeScript biết 'canvas' là một HTMLCanvasElement ở đây

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

Làm việc với Unknown Types

Khi làm việc với dữ liệu từ một nguồn bên ngoài, chẳng hạn như một API, bạn có thể nhận được dữ liệu với một kiểu không xác định. Bạn có thể sử dụng một type assertion để cho TypeScript biết cách xử lý dữ liệu.

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; // Khẳng định rằng dữ liệu là một User
}

fetchUser(1)
  .then(user => {
    console.log(user.name); // TypeScript biết 'user' là một User ở đây
  })
  .catch(error => {
    console.error("Error fetching user:", error);
  });

Những lưu ý khi sử dụng Type Assertions

Type assertions nên được sử dụng một cách tiết kiệm và thận trọng. Việc lạm dụng type assertions có thể che giấu các lỗi kiểu cơ bản và dẫn đến các vấn đề runtime. Dưới đây là một số cân nhắc quan trọng:

Type Narrowing

Type guards có liên kết mật thiết với khái niệm type narrowing. Type narrowing là quá trình tinh chỉnh kiểu của một biến thành một kiểu cụ thể hơn dựa trên các điều kiện hoặc kiểm tra runtime. Type guards là các công cụ chúng ta sử dụng để đạt được type narrowing.

TypeScript sử dụng phân tích luồng điều khiển để hiểu cách kiểu của một biến thay đổi trong các nhánh mã khác nhau. Khi một type guard được sử dụng, TypeScript cập nhật sự hiểu biết nội bộ của nó về kiểu của biến, cho phép bạn sử dụng một cách an toàn các phương thức và thuộc tính dành riêng cho kiểu đó.

Ví dụ về 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 biết 'value' là một chuỗi ở đây
  } else {
    console.log(value.toFixed(2)); // TypeScript biết 'value' là một số ở đây
  }
}

processValue("test"); // Output: TEST
processValue(123.456); // Output: 123.46
processValue(null); // Output: Value is null

Các phương pháp hay nhất

Để tận dụng hiệu quả type guards và type assertions trong các dự án TypeScript của bạn, hãy xem xét các phương pháp hay nhất sau:

Các cân nhắc quốc tế

Khi phát triển các ứng dụng cho đối tượng toàn cầu, hãy lưu ý đến cách type guards và type assertions có thể ảnh hưởng đến nỗ lực bản địa hóa và quốc tế hóa (i18n). Cụ thể, hãy xem xét:

Kết luận

Type guards và type assertions là những công cụ cần thiết để tăng cường tính an toàn kiểu và viết mã TypeScript mạnh mẽ hơn. Bằng cách hiểu cách sử dụng hiệu quả các tính năng này, bạn có thể ngăn ngừa các lỗi runtime, cải thiện khả năng bảo trì mã và tạo ra các ứng dụng đáng tin cậy hơn. Hãy nhớ ưu tiên type guards hơn type assertions bất cứ khi nào có thể, ghi lại type assertions của bạn và xác thực dữ liệu bên ngoài để đảm bảo tính chính xác của thông tin kiểu của bạn. Áp dụng các nguyên tắc này sẽ cho phép bạn tạo ra phần mềm ổn định và có thể dự đoán được hơn, phù hợp để triển khai trên toàn cầu.