Tiếng Việt

Khám phá kiểu literal trong TypeScript, một tính năng mạnh mẽ để thực thi các ràng buộc giá trị nghiêm ngặt, cải thiện độ rõ ràng của code và ngăn ngừa lỗi. Học qua các ví dụ thực tế và kỹ thuật nâng cao.

Kiểu Literal trong TypeScript: Làm chủ các Ràng buộc Giá trị Chính xác

TypeScript, một tập hợp cha của JavaScript, mang đến kiểu tĩnh cho thế giới phát triển web năng động. Một trong những tính năng mạnh mẽ nhất của nó là khái niệm về kiểu literal. Kiểu literal cho phép bạn chỉ định giá trị chính xác mà một biến hoặc thuộc tính có thể chứa, cung cấp độ an toàn kiểu nâng cao và ngăn ngừa các lỗi không mong muốn. Bài viết này sẽ khám phá sâu về kiểu literal, bao gồm cú pháp, cách sử dụng và lợi ích của chúng với các ví dụ thực tế.

Kiểu Literal là gì?

Không giống như các kiểu truyền thống như string, number, hoặc boolean, kiểu literal không đại diện cho một danh mục giá trị rộng lớn. Thay vào đó, chúng đại diện cho các giá trị cụ thể, cố định. TypeScript hỗ trợ ba loại kiểu literal:

Bằng cách sử dụng kiểu literal, bạn có thể tạo ra các định nghĩa kiểu chính xác hơn, phản ánh các ràng buộc thực tế của dữ liệu, dẫn đến mã nguồn mạnh mẽ và dễ bảo trì hơn.

Kiểu Literal Chuỗi

Kiểu literal chuỗi là loại literal được sử dụng phổ biến nhất. Chúng cho phép bạn chỉ định rằng một biến hoặc thuộc tính chỉ có thể chứa một trong một tập hợp các giá trị chuỗi được xác định trước.

Cú pháp Cơ bản

Cú pháp để định nghĩa một kiểu literal chuỗi rất đơn giản:


type AllowedValues = "value1" | "value2" | "value3";

Điều này định nghĩa một kiểu tên là AllowedValues chỉ có thể chứa các chuỗi "value1", "value2", hoặc "value3".

Ví dụ Thực tế

1. Định nghĩa Bảng màu:

Hãy tưởng tượng bạn đang xây dựng một thư viện UI và muốn đảm bảo rằng người dùng chỉ có thể chỉ định màu từ một bảng màu được xác định trước:


type Color = "red" | "green" | "blue" | "yellow";

function paintElement(element: HTMLElement, color: Color) {
  element.style.backgroundColor = color;
}

paintElement(document.getElementById("myElement")!, "red"); // Hợp lệ
paintElement(document.getElementById("myElement")!, "purple"); // Lỗi: Đối số kiểu '"purple"' không thể gán cho tham số kiểu 'Color'.

Ví dụ này minh họa cách kiểu literal chuỗi có thể thực thi một tập hợp nghiêm ngặt các giá trị được phép, ngăn các nhà phát triển vô tình sử dụng màu không hợp lệ.

2. Định nghĩa các Endpoint API:

Khi làm việc với API, bạn thường cần chỉ định các endpoint được phép. Kiểu literal chuỗi có thể giúp thực thi điều này:


type APIEndpoint = "/users" | "/posts" | "/comments";

function fetchData(endpoint: APIEndpoint) {
  // ... logic để lấy dữ liệu từ endpoint đã chỉ định
  console.log(`Đang lấy dữ liệu từ ${endpoint}`);
}

fetchData("/users"); // Hợp lệ
fetchData("/products"); // Lỗi: Đối số kiểu '"/products"' không thể gán cho tham số kiểu 'APIEndpoint'.

Ví dụ này đảm bảo rằng hàm fetchData chỉ có thể được gọi với các endpoint API hợp lệ, giảm nguy cơ lỗi do gõ sai hoặc tên endpoint không chính xác.

3. Xử lý các Ngôn ngữ khác nhau (Quốc tế hóa - i18n):

Trong các ứng dụng toàn cầu, bạn có thể cần xử lý các ngôn ngữ khác nhau. Bạn có thể sử dụng kiểu literal chuỗi để đảm bảo rằng ứng dụng của bạn chỉ hỗ trợ các ngôn ngữ đã chỉ định:


type Language = "en" | "es" | "fr" | "de" | "zh";

function translate(text: string, language: Language): string {
  // ... logic để dịch văn bản sang ngôn ngữ đã chỉ định
  console.log(`Đang dịch '${text}' sang ${language}`);
  return "Văn bản đã dịch"; // Giá trị giữ chỗ
}

translate("Hello", "en"); // Hợp lệ
translate("Hello", "ja"); // Lỗi: Đối số kiểu '"ja"' không thể gán cho tham số kiểu 'Language'.

Ví dụ này minh họa cách đảm bảo rằng chỉ các ngôn ngữ được hỗ trợ mới được sử dụng trong ứng dụng của bạn.

Kiểu Literal Số

Kiểu literal số cho phép bạn chỉ định rằng một biến hoặc thuộc tính chỉ có thể chứa một giá trị số cụ thể.

Cú pháp Cơ bản

Cú pháp để định nghĩa một kiểu literal số tương tự như kiểu literal chuỗi:


type StatusCode = 200 | 404 | 500;

Điều này định nghĩa một kiểu tên là StatusCode chỉ có thể chứa các số 200, 404, hoặc 500.

Ví dụ Thực tế

1. Định nghĩa Mã trạng thái HTTP:

Bạn có thể sử dụng kiểu literal số để đại diện cho các mã trạng thái HTTP, đảm bảo rằng chỉ các mã hợp lệ mới được sử dụng trong ứng dụng của bạn:


type HTTPStatus = 200 | 400 | 401 | 403 | 404 | 500;

function handleResponse(status: HTTPStatus) {
  switch (status) {
    case 200:
      console.log("Thành công!");
      break;
    case 400:
      console.log("Yêu cầu không hợp lệ");
      break;
    // ... các trường hợp khác
    default:
      console.log("Trạng thái không xác định");
  }
}

handleResponse(200); // Hợp lệ
handleResponse(600); // Lỗi: Đối số kiểu '600' không thể gán cho tham số kiểu 'HTTPStatus'.

Ví dụ này thực thi việc sử dụng các mã trạng thái HTTP hợp lệ, ngăn ngừa lỗi gây ra bởi việc sử dụng các mã không chính xác hoặc không theo tiêu chuẩn.

2. Đại diện cho các Tùy chọn Cố định:

Bạn có thể sử dụng kiểu literal số để đại diện cho các tùy chọn cố định trong một đối tượng cấu hình:


type RetryAttempts = 1 | 3 | 5;

interface Config {
  retryAttempts: RetryAttempts;
}

const config1: Config = { retryAttempts: 3 }; // Hợp lệ
const config2: Config = { retryAttempts: 7 }; // Lỗi: Kiểu '{ retryAttempts: 7; }' không thể gán cho kiểu 'Config'.

Ví dụ này giới hạn các giá trị có thể có cho retryAttempts trong một tập hợp cụ thể, cải thiện độ rõ ràng và độ tin cậy của cấu hình của bạn.

Kiểu Literal Boolean

Kiểu literal boolean đại diện cho các giá trị cụ thể true hoặc false. Mặc dù chúng có vẻ ít linh hoạt hơn so với kiểu literal chuỗi hoặc số, chúng có thể hữu ích trong các kịch bản cụ thể.

Cú pháp Cơ bản

Cú pháp để định nghĩa một kiểu literal boolean là:


type IsEnabled = true | false;

Tuy nhiên, việc sử dụng trực tiếp true | false là thừa vì nó tương đương với kiểu boolean. Kiểu literal boolean hữu ích hơn khi được kết hợp với các kiểu khác hoặc trong các kiểu điều kiện.

Ví dụ Thực tế

1. Logic Điều kiện với Cấu hình:

Bạn có thể sử dụng kiểu literal boolean để kiểm soát hành vi của một hàm dựa trên một cờ cấu hình:


interface FeatureFlags {
  darkMode: boolean;
  newUserFlow: boolean;
}

function initializeApp(flags: FeatureFlags) {
  if (flags.darkMode) {
    // Bật chế độ tối
    console.log("Đang bật chế độ tối...");
  } else {
    // Sử dụng chế độ sáng
    console.log("Đang sử dụng chế độ sáng...");
  }

  if (flags.newUserFlow) {
    // Bật luồng người dùng mới
    console.log("Đang bật luồng người dùng mới...");
  } else {
    // Sử dụng luồng người dùng cũ
    console.log("Đang sử dụng luồng người dùng cũ...");
  }
}

initializeApp({ darkMode: true, newUserFlow: false });

Mặc dù ví dụ này sử dụng kiểu boolean tiêu chuẩn, bạn có thể kết hợp nó với các kiểu điều kiện (sẽ giải thích sau) để tạo ra hành vi phức tạp hơn.

2. Union Phân Biệt (Discriminated Unions):

Kiểu literal boolean có thể được sử dụng làm yếu tố phân biệt trong các kiểu union. Hãy xem xét ví dụ sau:


interface SuccessResult {
  success: true;
  data: any;
}

interface ErrorResult {
  success: false;
  error: string;
}

type Result = SuccessResult | ErrorResult;

function processResult(result: Result) {
  if (result.success) {
    console.log("Thành công:", result.data);
  } else {
    console.error("Lỗi:", result.error);
  }
}

processResult({ success: true, data: { name: "John" } });
processResult({ success: false, error: "Không thể lấy dữ liệu" });

Ở đây, thuộc tính success, là một kiểu literal boolean, hoạt động như một yếu tố phân biệt, cho phép TypeScript thu hẹp kiểu của result trong câu lệnh if.

Kết hợp Kiểu Literal với Kiểu Union

Kiểu literal mạnh mẽ nhất khi được kết hợp với các kiểu union (sử dụng toán tử |). Điều này cho phép bạn định nghĩa một kiểu có thể chứa một trong nhiều giá trị cụ thể.

Ví dụ Thực tế

1. Định nghĩa Kiểu Trạng thái:


type Status = "pending" | "in progress" | "completed" | "failed";

interface Task {
  id: number;
  description: string;
  status: Status;
}

const task1: Task = { id: 1, description: "Implement login", status: "in progress" }; // Hợp lệ
const task2: Task = { id: 2, description: "Implement logout", status: "done" };       // Lỗi: Kiểu '{ id: number; description: string; status: string; }' không thể gán cho kiểu 'Task'.

Ví dụ này minh họa cách thực thi một tập hợp cụ thể các giá trị trạng thái được phép cho một đối tượng Task.

2. Định nghĩa Kiểu Thiết bị:

Trong một ứng dụng di động, bạn có thể cần xử lý các loại thiết bị khác nhau. Bạn có thể sử dụng một union của các kiểu literal chuỗi để đại diện cho chúng:


type DeviceType = "mobile" | "tablet" | "desktop";

function logDeviceType(device: DeviceType) {
  console.log(`Loại thiết bị: ${device}`);
}

logDeviceType("mobile"); // Hợp lệ
logDeviceType("smartwatch"); // Lỗi: Đối số kiểu '"smartwatch"' không thể gán cho tham số kiểu 'DeviceType'.

Ví dụ này đảm bảo rằng hàm logDeviceType chỉ được gọi với các loại thiết bị hợp lệ.

Kiểu Literal với Bí danh Kiểu (Type Aliases)

Bí danh kiểu (sử dụng từ khóa type) cung cấp một cách để đặt tên cho một kiểu literal, làm cho mã của bạn dễ đọc và dễ bảo trì hơn.

Ví dụ Thực tế

1. Định nghĩa Kiểu Mã tiền tệ:


type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";

function formatCurrency(amount: number, currency: CurrencyCode): string {
  // ... logic để định dạng số tiền dựa trên mã tiền tệ
  console.log(`Đang định dạng ${amount} bằng ${currency}`);
  return "Số tiền đã định dạng"; // Giá trị giữ chỗ
}

formatCurrency(100, "USD"); // Hợp lệ
formatCurrency(200, "CAD"); // Lỗi: Đối số kiểu '"CAD"' không thể gán cho tham số kiểu 'CurrencyCode'.

Ví dụ này định nghĩa một bí danh kiểu CurrencyCode cho một tập hợp các mã tiền tệ, cải thiện khả năng đọc của hàm formatCurrency.

2. Định nghĩa Kiểu Ngày trong Tuần:


type DayOfWeek = "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";

function isWeekend(day: DayOfWeek): boolean {
  return day === "Saturday" || day === "Sunday";
}

console.log(isWeekend("Monday"));   // false
console.log(isWeekend("Saturday")); // true
console.log(isWeekend("Funday"));   // Lỗi: Đối số kiểu '"Funday"' không thể gán cho tham số kiểu 'DayOfWeek'.

Suy luận Literal

TypeScript thường có thể tự động suy luận các kiểu literal dựa trên các giá trị bạn gán cho các biến. Điều này đặc biệt hữu ích khi làm việc với các biến const.

Ví dụ Thực tế

1. Suy luận Kiểu Literal Chuỗi:


const apiKey = "your-api-key"; // TypeScript suy luận kiểu của apiKey là "your-api-key"

function validateApiKey(key: "your-api-key") {
  return key === "your-api-key";
}

console.log(validateApiKey(apiKey)); // true

const anotherKey = "invalid-key";
console.log(validateApiKey(anotherKey)); // Lỗi: Đối số kiểu 'string' không thể gán cho tham số kiểu '"your-api-key"'.

Trong ví dụ này, TypeScript suy luận kiểu của apiKey là kiểu literal chuỗi "your-api-key". Tuy nhiên, nếu bạn gán một giá trị không phải hằng số cho một biến, TypeScript thường sẽ suy luận ra kiểu string rộng hơn.

2. Suy luận Kiểu Literal Số:


const port = 8080; // TypeScript suy luận kiểu của port là 8080

function startServer(portNumber: 8080) {
  console.log(`Đang khởi động máy chủ trên cổng ${portNumber}`);
}

startServer(port); // Hợp lệ

const anotherPort = 3000;
startServer(anotherPort); // Lỗi: Đối số kiểu 'number' không thể gán cho tham số kiểu '8080'.

Sử dụng Kiểu Literal với Kiểu Điều kiện

Kiểu literal trở nên mạnh mẽ hơn nữa khi được kết hợp với các kiểu điều kiện. Các kiểu điều kiện cho phép bạn định nghĩa các kiểu phụ thuộc vào các kiểu khác, tạo ra các hệ thống kiểu rất linh hoạt và biểu cảm.

Cú pháp Cơ bản

Cú pháp cho một kiểu điều kiện là:


TypeA extends TypeB ? TypeC : TypeD

Điều này có nghĩa là: nếu TypeA có thể gán cho TypeB, thì kiểu kết quả là TypeC; nếu không, kiểu kết quả là TypeD.

Ví dụ Thực tế

1. Ánh xạ Trạng thái sang Thông báo:


type Status = "pending" | "in progress" | "completed" | "failed";

type StatusMessage = T extends "pending"
  ? "Đang chờ hành động"
  : T extends "in progress"
  ? "Hiện đang xử lý"
  : T extends "completed"
  ? "Nhiệm vụ đã hoàn thành thành công"
  : "Đã xảy ra lỗi";

function getStatusMessage(status: T): StatusMessage {
  switch (status) {
    case "pending":
      return "Đang chờ hành động" as StatusMessage;
    case "in progress":
      return "Hiện đang xử lý" as StatusMessage;
    case "completed":
      return "Nhiệm vụ đã hoàn thành thành công" as StatusMessage;
    case "failed":
      return "Đã xảy ra lỗi" as StatusMessage;
    default:
      throw new Error("Trạng thái không hợp lệ");
  }
}

console.log(getStatusMessage("pending"));    // Đang chờ hành động
console.log(getStatusMessage("in progress")); // Hiện đang xử lý
console.log(getStatusMessage("completed"));   // Nhiệm vụ đã hoàn thành thành công
console.log(getStatusMessage("failed"));      // Đã xảy ra lỗi

Ví dụ này định nghĩa một kiểu StatusMessage ánh xạ mỗi trạng thái có thể có tới một thông báo tương ứng bằng cách sử dụng các kiểu điều kiện. Hàm getStatusMessage tận dụng kiểu này để cung cấp các thông báo trạng thái an toàn về kiểu.

2. Tạo một Trình xử lý Sự kiện An toàn về Kiểu:


type EventType = "click" | "mouseover" | "keydown";

type EventData = T extends "click"
  ? { x: number; y: number; } // Dữ liệu sự kiện click
  : T extends "mouseover"
  ? { target: HTMLElement; }   // Dữ liệu sự kiện mouseover
  : { key: string; }             // Dữ liệu sự kiện keydown

function handleEvent(type: T, data: EventData) {
  console.log(`Đang xử lý sự kiện loại ${type} với dữ liệu:`, data);
}

handleEvent("click", { x: 10, y: 20 }); // Hợp lệ
handleEvent("mouseover", { target: document.getElementById("myElement")! }); // Hợp lệ
handleEvent("keydown", { key: "Enter" }); // Hợp lệ

handleEvent("click", { key: "Enter" }); // Lỗi: Đối số của kiểu '{ key: string; }' không thể gán cho tham số của kiểu '{ x: number; y: number; }'.

Ví dụ này tạo ra một kiểu EventData định nghĩa các cấu trúc dữ liệu khác nhau dựa trên loại sự kiện. Điều này cho phép bạn đảm bảo rằng dữ liệu chính xác được truyền cho hàm handleEvent cho mỗi loại sự kiện.

Các Thực hành Tốt nhất khi Sử dụng Kiểu Literal

Để sử dụng hiệu quả các kiểu literal trong các dự án TypeScript của bạn, hãy xem xét các thực hành tốt nhất sau:

Lợi ích của việc Sử dụng Kiểu Literal

Kết luận

Kiểu literal trong TypeScript là một tính năng mạnh mẽ cho phép bạn thực thi các ràng buộc giá trị nghiêm ngặt, cải thiện độ rõ ràng của code và ngăn ngừa lỗi. Bằng cách hiểu cú pháp, cách sử dụng và lợi ích của chúng, bạn có thể tận dụng kiểu literal để tạo ra các ứng dụng TypeScript mạnh mẽ và dễ bảo trì hơn. Từ việc định nghĩa bảng màu và các endpoint API đến việc xử lý các ngôn ngữ khác nhau và tạo ra các trình xử lý sự kiện an toàn về kiểu, kiểu literal cung cấp một loạt các ứng dụng thực tế có thể nâng cao đáng kể quy trình phát triển của bạn.