Tiếng Việt

Khai phá sức mạnh của cấu trúc dữ liệu bất biến trong TypeScript với kiểu readonly. Tìm hiểu cách tạo ra các ứng dụng dễ dự đoán, dễ bảo trì và bền vững hơn bằng cách ngăn chặn các thay đổi dữ liệu ngoài ý muốn.

Kiểu Dữ liệu Readonly trong TypeScript: Làm chủ Cấu trúc Dữ liệu Bất biến

Trong bối cảnh phát triển phần mềm không ngừng thay đổi, việc theo đuổi mã nguồn bền vững, dễ dự đoán và dễ bảo trì là một nỗ lực không ngừng. TypeScript, với hệ thống kiểu dữ liệu mạnh mẽ, cung cấp các công cụ đắc lực để đạt được những mục tiêu này. Trong số các công cụ đó, kiểu readonly nổi bật như một cơ chế quan trọng để thực thi tính bất biến, một nền tảng của lập trình hàm và là chìa khóa để xây dựng các ứng dụng đáng tin cậy hơn.

Tính bất biến là gì và Tại sao nó lại quan trọng?

Tính bất biến, về cơ bản, có nghĩa là một khi một đối tượng được tạo ra, trạng thái của nó không thể bị thay đổi. Khái niệm đơn giản này có ý nghĩa sâu sắc đối với chất lượng mã nguồn và khả năng bảo trì.

Các kiểu Readonly trong TypeScript: Kho vũ khí bất biến của bạn

TypeScript cung cấp một số cách để thực thi tính bất biến bằng cách sử dụng từ khóa readonly. Hãy cùng khám phá các kỹ thuật khác nhau và cách chúng có thể được áp dụng trong thực tế.

1. Thuộc tính Readonly trên Interfaces và Types

Cách đơn giản nhất để khai báo một thuộc tính là chỉ đọc (readonly) là sử dụng từ khóa readonly trực tiếp trong định nghĩa interface hoặc type.


interface Person {
  readonly id: string;
  name: string;
  age: number;
}

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // Lỗi: Không thể gán cho 'id' vì đây là một thuộc tính chỉ đọc.
person.name = "Bob"; // Điều này được phép

Trong ví dụ này, thuộc tính id được khai báo là readonly. TypeScript sẽ ngăn chặn mọi nỗ lực sửa đổi nó sau khi đối tượng được tạo. Các thuộc tính nameage, không có bổ từ readonly, có thể được sửa đổi tự do.

2. Kiểu tiện ích Readonly

TypeScript cung cấp một kiểu tiện ích mạnh mẽ được gọi là Readonly<T>. Kiểu generic này nhận vào một kiểu T hiện có và biến đổi nó bằng cách làm cho tất cả các thuộc tính của nó trở thành readonly.


interface Point {
  x: number;
  y: number;
}

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // Lỗi: Không thể gán cho 'x' vì đây là một thuộc tính chỉ đọc.

Kiểu Readonly<Point> tạo ra một kiểu mới trong đó cả xy đều là readonly. Đây là một cách tiện lợi để nhanh chóng làm cho một kiểu hiện có trở nên bất biến.

3. Mảng chỉ đọc (ReadonlyArray<T>) và readonly T[]

Mảng trong JavaScript vốn có thể thay đổi được. TypeScript cung cấp một cách để tạo các mảng chỉ đọc bằng cách sử dụng kiểu ReadonlyArray<T> hoặc cú pháp viết tắt readonly T[]. Điều này ngăn chặn việc sửa đổi nội dung của mảng.


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // Lỗi: Thuộc tính 'push' không tồn tại trên kiểu 'readonly number[]'.
// numbers[0] = 10; // Lỗi: Chữ ký chỉ mục trong kiểu 'readonly number[]' chỉ cho phép đọc.

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // Tương đương với ReadonlyArray
// moreNumbers.push(11); // Lỗi: Thuộc tính 'push' không tồn tại trên kiểu 'readonly number[]'.

Việc cố gắng sử dụng các phương thức sửa đổi mảng, chẳng hạn như push, pop, splice, hoặc gán trực tiếp vào một chỉ mục, sẽ dẫn đến lỗi TypeScript.

4. constreadonly: Hiểu rõ sự khác biệt

Điều quan trọng là phải phân biệt giữa constreadonly. const ngăn chặn việc gán lại giá trị cho chính biến đó, trong khi readonly ngăn chặn việc sửa đổi các thuộc tính của đối tượng. Chúng phục vụ các mục đích khác nhau và có thể được sử dụng cùng nhau để đạt được tính bất biến tối đa.


const immutableNumber = 42;
// immutableNumber = 43; // Lỗi: Không thể gán lại cho biến const 'immutableNumber'.

const mutableObject = { value: 10 };
mutableObject.value = 20; // Điều này được phép vì *đối tượng* không phải là const, chỉ có biến là const.

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // Lỗi: Không thể gán cho 'value' vì đây là một thuộc tính chỉ đọc.

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // Lỗi: Không thể gán lại cho biến const 'constReadonlyObject'.
// constReadonlyObject.value = 60; // Lỗi: Không thể gán cho 'value' vì đây là một thuộc tính chỉ đọc.

Như đã trình bày ở trên, const đảm bảo biến luôn trỏ đến cùng một đối tượng trong bộ nhớ, trong khi readonly đảm bảo rằng trạng thái bên trong của đối tượng không bị thay đổi.

Ví dụ thực tế: Áp dụng các kiểu Readonly trong các tình huống thực tế

Hãy cùng khám phá một số ví dụ thực tế về cách các kiểu readonly có thể được sử dụng để nâng cao chất lượng mã và khả năng bảo trì trong các tình huống khác nhau.

1. Quản lý dữ liệu cấu hình

Dữ liệu cấu hình thường được tải một lần khi ứng dụng khởi động và không nên bị sửa đổi trong quá trình chạy. Sử dụng các kiểu readonly đảm bảo rằng dữ liệu này vẫn nhất quán và ngăn chặn các sửa đổi vô tình.


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... sử dụng config.timeout và config.apiUrl một cách an toàn, biết rằng chúng sẽ không thay đổi
}

fetchData("/data", config);

2. Triển khai Quản lý Trạng thái kiểu Redux

Trong các thư viện quản lý trạng thái như Redux, tính bất biến là một nguyên tắc cốt lõi. Các kiểu readonly có thể được sử dụng để đảm bảo rằng trạng thái vẫn bất biến và các reducer chỉ trả về các đối tượng trạng thái mới thay vì sửa đổi những đối tượng hiện có.


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // Trả về một đối tượng trạng thái mới
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // Trả về một đối tượng trạng thái mới với các mục đã được cập nhật
    default:
      return state;
  }
}

3. Làm việc với Phản hồi từ API

Khi tìm nạp dữ liệu từ API, thường mong muốn coi dữ liệu phản hồi là bất biến, đặc biệt nếu bạn đang sử dụng nó để render các thành phần giao diện người dùng. Các kiểu readonly có thể giúp ngăn chặn các thay đổi vô tình đối với dữ liệu API.


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // Lỗi: Không thể gán cho 'completed' vì đây là một thuộc tính chỉ đọc.
});

4. Mô hình hóa Dữ liệu Địa lý (Ví dụ Quốc tế)

Hãy xem xét việc biểu diễn tọa độ địa lý. Một khi tọa độ đã được thiết lập, lý tưởng nhất là nó không đổi. Điều này đảm bảo tính toàn vẹn của dữ liệu, đặc biệt khi xử lý các ứng dụng nhạy cảm như hệ thống bản đồ hoặc điều hướng hoạt động trên các khu vực địa lý khác nhau (ví dụ: tọa độ GPS cho một dịch vụ giao hàng hoạt động khắp Bắc Mỹ, Châu Âu và Châu Á).


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // Hãy tưởng tượng một phép tính phức tạp sử dụng vĩ độ và kinh độ
 // Trả về giá trị giữ chỗ cho đơn giản
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("Distance between Tokyo and New York (placeholder):", distance);

// tokyoCoordinates.latitude = 36.0; // Lỗi: Không thể gán cho 'latitude' vì đây là một thuộc tính chỉ đọc.

Kiểu Deeply Readonly: Xử lý các đối tượng lồng nhau

Kiểu tiện ích Readonly<T> chỉ làm cho các thuộc tính trực tiếp của một đối tượng trở thành readonly. Nếu một đối tượng chứa các đối tượng hoặc mảng lồng nhau, các cấu trúc lồng nhau đó vẫn có thể thay đổi được. Để đạt được tính bất biến sâu thực sự, bạn cần áp dụng Readonly<T> một cách đệ quy cho tất cả các thuộc tính lồng nhau.

Đây là một ví dụ về cách tạo một kiểu readonly sâu:


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // Lỗi
// company.address.city = "New City"; // Lỗi
// company.employees.push("Charlie"); // Lỗi

Kiểu DeepReadonly<T> này áp dụng Readonly<T> một cách đệ quy cho tất cả các thuộc tính lồng nhau, đảm bảo rằng toàn bộ cấu trúc đối tượng là bất biến.

Những điều cần cân nhắc và Sự đánh đổi

Mặc dù tính bất biến mang lại những lợi ích đáng kể, điều quan trọng là phải nhận thức được những sự đánh đổi tiềm tàng.

Các thư viện cho Cấu trúc Dữ liệu Bất biến

Một số thư viện có thể đơn giản hóa việc làm việc với các cấu trúc dữ liệu bất biến trong TypeScript:

Các thực hành tốt nhất khi sử dụng Kiểu Readonly

Để tận dụng hiệu quả các kiểu readonly trong các dự án TypeScript của bạn, hãy tuân theo các thực hành tốt nhất sau:

Kết luận: Nắm bắt tính bất biến với các kiểu Readonly của TypeScript

Các kiểu readonly của TypeScript là một công cụ mạnh mẽ để xây dựng các ứng dụng dễ dự đoán, dễ bảo trì và bền vững hơn. Bằng cách nắm bắt tính bất biến, bạn có thể giảm nguy cơ xảy ra lỗi, đơn giản hóa việc gỡ lỗi và cải thiện chất lượng tổng thể của mã nguồn. Mặc dù có một số đánh đổi cần xem xét, lợi ích của tính bất biến thường vượt trội hơn chi phí, đặc biệt là trong các dự án phức tạp và có vòng đời dài. Khi bạn tiếp tục hành trình với TypeScript, hãy biến các kiểu readonly thành một phần trung tâm trong quy trình phát triển của bạn để khai phá toàn bộ tiềm năng của tính bất biến và xây dựng phần mềm thực sự đáng tin cậy.