0

Series Total TypeScript Thực chiến #3: Template Literal Types – Trò chơi "Xếp hình" với Chuỗi văn bản

1. Giới hạn của Union Types thông thường

Hãy tưởng tượng bạn đang viết một hệ thống quản lý sự kiện (Event Emitter) cho Backend. Các sự kiện xoay quanh các thực thể như user, order, product với các hành động như Created, Updated, Deleted.

Nếu dùng cách thông thường, bạn sẽ phải liệt kê toàn bộ tổ hợp bằng tay:

// ❌ Rất thủ công và khó bảo trì
type AppEvent = 
  | 'userCreated' | 'userUpdated' | 'userDeleted'
  | 'orderCreated' | 'orderUpdated' | 'orderDeleted'
  | 'productCreated' | 'productUpdated' | 'productDeleted';

Nếu ngày mai hệ thống có thêm thực thể invoice (hóa đơn), bạn lại phải gõ thêm 3 dòng nữa. Cách làm này vừa chậm, vừa có nguy cơ gõ sai chính tả.

2. Phép màu của Template Literal Types

Trong JavaScript ES6, chúng ta dùng Template Literals (dấu backtick ``) để nối chuỗi giá trị:

const name = "Thế giới";
console.log(`Chào ${name}!`); // "Chào Thế giới!"

TypeScript mang đúng cú pháp này vào không gian Type. Bạn có thể nhúng một Union Type vào bên trong một chuỗi để tạo ra một Type mới.

type Action = 'Created' | 'Updated' | 'Deleted';

// ✅ Sử dụng Template Literal Type
type UserEvent = `user${Action}`;

// Lúc này, UserEvent tự động bung ra thành:
// 'userCreated' | 'userUpdated' | 'userDeleted'

3. Tổ hợp chéo (Permutations) – Sức mạnh thực sự

Điều kỳ diệu nhất của Template Literal Types xảy ra khi bạn nhúng nhiều Union Types vào cùng một chuỗi. TypeScript sẽ tự động tính toán tổ hợp chéo (Cartesian product) của tất cả các phần tử.

Quay lại bài toán Event Emitter ở phần 1, chúng ta chỉ cần viết như sau:

type Entity = 'user' | 'order' | 'product';
type Action = 'Created' | 'Updated' | 'Deleted';

// 🔥 Nổ tung không gian Type
type AppEvent = `${Entity}${Action}`;

/*
AppEvent sẽ tự động nội suy ra toàn bộ 9 trường hợp:
'userCreated' | 'userUpdated' | 'userDeleted' |
'orderCreated' | 'orderUpdated' | 'orderDeleted' |
'productCreated' | 'productUpdated' | 'productDeleted'
*/

function handleEvent(event: AppEvent) {
  // Logic xử lý
}

handleEvent('orderUpdated'); // ✅ Ngon lành
handleEvent('userPayed'); // ❌ IDE báo lỗi ngay: Argument of type '"userPayed"' is not assignable...

Chỉ với 3 dòng code ngắn gọn, bạn đã tạo ra một bộ quy tắc cực kỳ chặt chẽ. Khi thêm thực thể invoice vào type Entity, Type AppEvent sẽ tự động phình to ra thành 12 trường hợp.

4. Các Utility Types tích hợp sẵn cho Chuỗi

Đôi khi, quy ước đặt tên (Naming Convention) của công ty yêu cầu bạn phải viết hoa chữ cái đầu, hoặc viết hoa toàn bộ. TypeScript đã tích hợp sẵn 4 Utility Types chuyên dùng để thao tác với chuỗi ngay ở cấp độ Type:

  • Uppercase<StringType>
  • Lowercase<StringType>
  • Capitalize<StringType>
  • Uncapitalize<StringType>

Hãy xem cách kết hợp chúng với Template Literals để chuẩn hóa API Routes:

type Module = 'Users' | 'Products' | 'Invoices';

// Yêu cầu: Sinh ra các API endpoint dạng chữ thường, ví dụ: "/api/users"

// Bước 1: Đưa Module về chữ thường
type LowercaseModule = Lowercase<Module>; // 'users' | 'products' | 'invoices'

// Bước 2: Ghép vào chuỗi route
type ApiRoute = `/api/${LowercaseModule}`;

// Kết quả: '/api/users' | '/api/products' | '/api/invoices'

Hoặc một bài toán kinh điển trong việc tự động sinh type cho các hàm Redux / Vuex Getters:

type StateKeys = 'username' | 'email' | 'theme';

// Yêu cầu: Sinh ra các hàm getUsername, getEmail, getTheme
type GetterNames = `get${Capitalize<StateKeys>}`;

// Kết quả GetterNames chính xác là: 'getUsername' | 'getEmail' | 'getTheme'

Bằng việc nắm vững Template Literal Types, bạn đã biến hệ thống Type của TypeScript thành một cỗ máy sản xuất mã tự động. Nó giúp loại bỏ hoàn toàn các lỗi hard-code chuỗi (string typoes) vốn rất khó debug trong các dự án Backend lớn. Việc bảo trì giờ đây chỉ đơn giản là thêm/bớt từ khóa ở một nguồn duy nhất.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí