Series Total TypeScript Thực chiến #2: Tuyệt kỹ Indexed Access – "Móc ruột" Array và Object
1. Vấn đề: Trích xuất Type từ một Mảng (Array)
Giả sử trong hệ thống Backend Node.js, bạn định nghĩa một mảng chứa các phương thức HTTP được phép đi qua Middleware:
// Thêm 'as const' để ép TypeScript hiểu đây là một Tuple cố định,
// không phải là mảng string[] thông thường có thể thêm bớt.
const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const;
Bây giờ, bạn muốn viết một hàm nhận vào method. Bạn cần một Type là 'GET' | 'POST' | 'PUT' | 'DELETE'. Nếu bạn làm thủ công, bạn sẽ lại phải gõ tay: type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'. Code lại bị nhân bản (duplicate) và dễ out-of-sync.
2. Thuốc giải: Truy cập bằng Chỉ mục [number]
Trong JavaScript, để lấy một phần tử trong mảng, bạn truyền vào một con số (index): ALLOWED_METHODS[0] (trả về 'GET'). Trong thế giới Type của TypeScript, để lấy ra tất cả các phần tử của một mảng dưới dạng Union Type, bạn truyền vào từ khóa [number].
Kết hợp với tuyệt kỹ typeof ở bài trước, ta có cú pháp rút trích như sau:
const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const;
// 🔥 Tuyệt kỹ móc type từ Array
type HttpMethod = typeof ALLOWED_METHODS[number];
// Kết quả HttpMethod chính xác là: 'GET' | 'POST' | 'PUT' | 'DELETE'
function handleRequest(method: HttpMethod) {
// Logic xử lý
}
handleRequest('POST'); // ✅ Hợp lệ
handleRequest('PATCH'); // ❌ Báo lỗi ngay lập tức
Cú pháp [number] báo cho TypeScript biết rằng: "Hãy lấy Type của bất kỳ phần tử nào có thể được truy cập bằng một con số chỉ mục trong mảng này".
3. Đào sâu vào Object lồng nhau (Lookup Types)
Indexed Access không chỉ dùng cho Array, nó còn là công cụ hoàn hảo để "móc ruột" các Object có cấu trúc sâu.
Trong dự án, rất nhiều khi bạn gọi một API từ bên thứ 3 (hoặc query từ Database) và nhận về một Type khổng lồ, nhưng bạn chỉ muốn lấy cấu trúc của một phần tử con bên trong đó để dùng cho hàm của mình.
// Một Type khổng lồ sinh ra từ việc query Database
type UserProfileResponse = {
id: string;
name: string;
contact: {
email: string;
phone: string;
isVerified: boolean;
};
permissions: string[];
};
Nếu bạn muốn viết một hàm chỉ nhận vào phần contact để xử lý việc gửi mã OTP, bạn không cần (và không nên) tạo mới một type Contact = { email: string; ... }. Hãy dùng thẳng Indexed Access bằng tên key:
// 🔥 Móc type từ Object
type ContactInfo = UserProfileResponse['contact'];
function sendOtp(contact: ContactInfo) {
if (contact.isVerified) {
console.log(`Gửi SMS tới ${contact.phone}`);
}
}
Bằng cách truy cập ['contact'], ContactInfo sẽ tự động sao chép nguyên xi cấu trúc của khối contact. Nếu UserProfileResponse thay đổi (thêm trường telegramId vào contact), hàm sendOtp của bạn sẽ lập tức nhận biết được cấu trúc mới mà không cần sửa dòng code định nghĩa type nào.
4. Kết hợp Mảng và Object: "Cú đúp" Indexed Access
Hãy xem một bài toán phức tạp hơn mang đậm phong cách Matt Pocock. Bạn có một mảng chứa danh sách các cấu hình nhà ga:
const METRO_STATIONS = [
{ id: 'S01', name: 'Bến Thành', isHub: true },
{ id: 'S02', name: 'Nhà hát Thành phố', isHub: false },
{ id: 'S03', name: 'Ba Son', isHub: false },
] as const;
Yêu cầu: Bắt TypeScript tự đẻ ra một Type chứa danh sách các id hợp lệ ('S01' | 'S02' | 'S03').
Chúng ta kết hợp tuần tự 2 bước:
- Dùng typeof METRO_STATIONS[number] để lấy ra type của toàn bộ object bên trong mảng.
- Tiếp tục dùng ['id'] để bóc lấy riêng cái value của trường id.
// 🔥 Móc nối tiếp (Chaining)
type StationId = typeof METRO_STATIONS[number]['id'];
// Kết quả StationId chính là: 'S01' | 'S02' | 'S03'
function getStationDetails(id: StationId) {
// ...
}
Bằng việc thành thạo Indexed Access Types ([number] và ['key']), bạn có thể linh hoạt nhào nặn và trích xuất bất kỳ ngóc ngách nào của dữ liệu. Type của bạn giờ đây không còn là những dòng code khô khan được gõ tay, mà là những công thức tự động nội suy từ chính luồng chạy của chương trình. Điều này đảm bảo tính nhất quán tuyệt đối cho mọi dịch vụ Backend Node.js mà bạn xây dựng.
All rights reserved