Peeps Avatar

Hello, codestus.

Go back

Xử lý Union Arrays với TypeScript Type Guards

Published at: 06/06/2024

5 mins read

Giới Thiệu

Trong quá trình phát triển frontend, việc xử lý các mảng phức tạp chứa các loại phần tử khác nhau là một nhiệm vụ phổ biến. TypeScript có thể cung cấp một lớp an toàn về kiểu dữ liệu khi làm việc với các mảng này. Tuy nhiên, việc phản ánh đúng điều này trong hệ thống kiểu có thể gặp khó khăn.

Ví Dụ

Chúng ta bắt đầu với việc định nghĩa một số kiểu đại diện cho các hình dạng khác nhau.

// Hình vuông với thuộc tính kích thước của bốn cạnh.
interface Square {
  type: "SQUARE";
  size: number;
}

// Hình chữ nhật với thuộc tính chiều cao và chiều rộng.
interface Rectangle {
  type: "RECTANGLE";
  height: number;
  width: number;
}

// Hình tròn với thuộc tính bán kính.
interface Circle {
  type: "CIRCLE";
  radius: number;
}

// Kiểu hợp của tất cả các hình dạng có thể có.
type Shape = Square | Rectangle | Circle;

Bây giờ, chúng ta có thể sử dụng các kiểu này để định nghĩa một số hình dạng và thêm chúng vào một mảng.

const circle1: Circle = { type: "CIRCLE", radius: 314 };
const circle2: Circle = { type: "CIRCLE", radius: 42 };
const square1: Square = { type: "SQUARE", size: 10 };
const square2: Square = { type: "SQUARE", size: 1 };
const rectangle1: Rectangle = { type: "RECTANGLE", height: 10, width: 4 };
const rectangle2: Rectangle = { type: "RECTANGLE", height: 3, width: 5 };

const shapes = [circle1, square1, rectangle1, square2, circle2, rectangle2];

Thách Thức

Bây giờ, hãy tìm hình vuông đầu tiên trong mảng này bằng phương thức find.

const firstSquare = shapes.find((shape) => shape.type === "SQUARE");
console.log(firstSquare);

Điều này sẽ in ra hình vuông đầu tiên (square1) như mong đợi:

{
  "type": "SQUARE",
  "size": 10
}

Tuy nhiên, chúng ta sẽ gặp rắc rối nếu cố truy cập thuộc tính size của hình vuông.

const firstSquare = shapes.find((shape) => shape.type === "SQUARE");
console.log(firstSquare?.size);
//                       ^^^^
// Property 'size' does not exist on type 'Square | Rectangle | Circle'.
//  Property 'size' does not exist on type 'Rectangle'.(2339)

Cách Giải Quyết

Sử Dụng Ép Kiểu (Casts)

Giải pháp đơn giản nhất là ép kiểu kết quả của find.

const firstCircle = shapes.find((shape) => shape.type === "SQUARE") as Square;
console.log(firstCircle?.size);

Kiểu Bảo Vệ (Type Guards)

TypeScript cung cấp nhiều cách để thu hẹp kiểu dữ liệu. Chúng ta có thể định nghĩa các kiểu bảo vệ cho từng hình dạng.

const isSquare = (shape: Shape): shape is Square => shape.type === "SQUARE";
const isCircle = (shape: Shape): shape is Circle => shape.type === "CIRCLE";
const isRectangle = (shape: Shape): shape is Rectangle => shape.type === "RECTANGLE";

Sử dụng kiểu bảo vệ này với các câu lệnh if để truy cập an toàn các thuộc tính chỉ có trên một hình dạng cụ thể.

const shape: Shape = square1;

if (isSquare(shape)) {
  console.log(shape.size);
}

Nâng Cao

Phương Thức Overload

Phương thức find có thể sử dụng một khai báo overload để thu hẹp kiểu dữ liệu.

interface Array<T> {
  find<S extends T>(
    predicate: (value: T, index: number, obj: T[]) => value is S,
    thisArg?: any
  ): S | undefined;

  find(
    predicate: (value: T, index: number, obj: T[]) => unknown,
    thisArg?: any
  ): T | undefined;
}

Chúng ta có thể truyền trực tiếp kiểu bảo vệ vào phương thức find.

const firstSquare = shapes.find(isSquare);
console.log(firstSquare?.size);

Gotcha

Kiểu bảo vệ cần được truyền trực tiếp vào phương thức find để hoạt động đúng cách.

const firstCircle = shapes.find((shape) => isCircle(shape));
console.log(firstCircle?.radius);
//                       ^^^^^^
// Property 'radius' does not exist on type 'Square | Rectangle | Circle'.

Phương Thức filter

Phương thức filter cung cấp hai overload tương tự find.

interface Array<T> {
  filter<S extends T>(
    predicate: (value: T, index: number, array: T[]) => value is S,
    thisArg?: any
  ): S[];

  filter(
    predicate: (value: T, index: number, array: T[]) => unknown,
    thisArg?: any
  ): T[];
}

Ví dụ:

const onlyCircles = shapes.filter(isCircle);
onlyCircles.forEach((circle) => console.log(circle.radius));

Kết Hợp

Nếu muốn tìm hình vuông với kích thước cụ thể, bạn có thể kết hợp filterfind.

const sizeOneSquare = shapes.find(
  (shape) => isSquare(shape) && shape.size === 1
);
console.log(sizeOneSquare?.size);

Tổng Kết

Khi làm việc với các mảng chứa phần tử hỗn hợp, hãy xem xét việc sử dụng kiểu bảo vệ để cải thiện an toàn kiểu dữ liệu. Điều này không chỉ giúp mã nguồn rõ ràng hơn mà còn tránh được các lỗi tiềm ẩn.