Indexed access type tra kiểu của một thuộc tính trên một kiểu khác, với đúng cú pháp ngoặc vuông mà JavaScript dùng để truy cập thuộc tính. Person["age"] là number, còn Person["address"]["city"] là string.
Lợi ích là chỉ có một nguồn sự thật duy nhất. Age không phải một bản sao thứ hai của number: nếu Person["age"] đổi thành string, mọi kiểu được viết là Person["age"] cũng đổi theo. Indexed access type chỉ tồn tại lúc biên dịch; chúng không sinh ra dòng JavaScript nào.
Khóa dạng union và T[keyof T]
Chỉ số có thể là một union các khóa. Kết quả là union của các kiểu thuộc tính tương ứng:
Product[keyof Product] là cách nói ở cấp kiểu cho "một giá trị thuộc tính bất kỳ của Product". Hỏi một khóa không tồn tại sẽ gây lỗi biên dịch: Product["price"] cho ra error TS2339: Property 'price' does not exist on type 'Product'.
Thuộc tính tùy chọn có kèm undefined
Khi bật strictNullChecks (được bật sẵn trong strict), kiểu của một thuộc tính tùy chọn có kèm undefined, và indexed access type của nó cũng vậy:
type Profile = { name: string; nickname?: string };
type Nick = Profile["nickname"]; // string | undefined
Để bỏ undefined, bọc nó lại: NonNullable<Profile["nickname"]> là string.
Kiểu phần tử mảng với T[number]
Mảng được đánh chỉ số bằng số, nên lấy chỉ số kiểu mảng bằng number sẽ cho ra kiểu phần tử. Cách này cũng dùng được qua nhiều cấp:
Cách này hữu ích khi một kiểu đến từ nơi bạn không kiểm soát, chẳng hạn một API client được sinh tự động: ApiResponse["data"]["users"][number] gọi tên kiểu user mà không cần ai export nó ra.
Tuple: lấy chỉ số theo vị trí
Một kiểu tuple có thể được lấy chỉ số bằng một vị trí cụ thể, bằng number để có union của mọi kiểu phần tử, và bằng "length" để có độ dài dưới dạng kiểu literal:
(typeof arr)[number]: union từ một mảng const
Cách dùng phổ biến nhất trong thực tế: giữ danh sách các giá trị hợp lệ trong một mảng (để có thể lặp qua lúc chạy) và suy ra union type từ nó (để trình biên dịch kiểm tra).
Có hai phần làm nên điều này. as const biến mảng thành một tuple readonly gồm các kiểu literal; không có nó, ROLES là string[] và (typeof ROLES)[number] chỉ là string. Sau đó typeof biến giá trị thành kiểu, và [number] gom các phần tử thành một union. Dấu ngoặc tròn là tùy chọn (typeof ROLES[number] có cùng nghĩa), nhưng chúng làm rõ thứ tự thực hiện.
Kiểm tra một chuỗi với mảng
Tuple readonly đó có một điểm bất ngờ. Phương thức includes của nó chỉ nhận kiểu phần tử, nên bạn không thể truyền một chuỗi bất kỳ vào:
index.ts(6,25): error TS2345: Argument of type 'string' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
Mở rộng mảng thành readonly string[] khi kiểm tra, và biến hàm thành một type guard để một lần kiểm tra thành công sẽ thu hẹp kiểu của giá trị:
Mẫu này cho bạn một danh sách dùng cho cả hai việc: phần kiểm tra lúc chạy dùng mảng, còn kiểu được suy ra từ chính mảng đó, nên hai bên không bao giờ lệch nhau.
Indexed access trong generic: T[K]
Bên trong một generic, T[K] với K extends keyof T là kiểu của thuộc tính mà nơi gọi đã chọn:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const users = [
{ name: "Ada", age: 36 },
{ name: "Linus", age: 28 },
];
const names = pluck(users, "name"); // string[]
const ages = pluck(users, "age"); // number[]
Ràng buộc K extends keyof T là thứ làm cho T[K] hợp lệ. Thiếu nó, TypeScript không thể biết K là một khóa của T và báo error TS2536: Type 'K' cannot be used to index type 'T'.
Tóm tắt cú pháp
| Cách viết | Ý nghĩa | Ví dụ kết quả |
|---|---|---|
T["key"] | kiểu của một thuộc tính | Person["age"] là number |
T["a" | "b"] | union các kiểu của những thuộc tính đó | string | number |
T[keyof T] | union của mọi kiểu thuộc tính | mọi kiểu giá trị |
T["a"]["b"] | thuộc tính lồng nhau | Person["address"]["city"] |
Arr[number] | kiểu phần tử mảng | User[] cho ra User |
Tup[0] | phần tử tuple tại một vị trí | kiểu của phần tử đầu tiên |
Tup["length"] | độ dài tuple dưới dạng literal | 3 |
(typeof arr)[number] | union các giá trị của một mảng const | "admin" | "editor" |
T[typeof key] | lấy chỉ số bằng kiểu của một hằng số | giống T["name"] |
Chỉ số luôn là một kiểu. Person[key] với key là một biến const sẽ bị hai lỗi: error TS2538: Type 'key' cannot be used as an index type. và error TS2749: 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?.
Câu hỏi thường gặp
Indexed access type trong TypeScript là gì?
Là kiểu được viết T[K], dùng để tra kiểu của thuộc tính K trên kiểu T. Với type Person = { name: string; age: number }, Person["age"] là number. Cú pháp ngoặc vuông giống hệt cách truy cập thuộc tính trong JavaScript, nhưng hoạt động trên kiểu ở thời điểm biên dịch.
Làm sao lấy kiểu phần tử của một mảng trong TypeScript?
Lấy chỉ số kiểu mảng bằng number: với type Users = User[], Users[number] là User. Với một giá trị mảng, kết hợp thêm typeof: (typeof users)[number].
Làm sao biến một mảng chuỗi thành union type?
Khai báo mảng với as const để TypeScript giữ nguyên các giá trị literal, rồi lấy chỉ số bằng number: const roles = ["admin", "editor"] as const; type Role = (typeof roles)[number]; cho ra "admin" | "editor". Không có as const thì kiểu phần tử chỉ là string.
Vì sao không dùng được biến làm chỉ số trong indexed access type?
Chỉ số phải là một kiểu. Person[key] với const key = "name" bị lỗi (TS2538 và TS2749) vì key là một giá trị. Hãy viết Person[typeof key], hoặc dùng thẳng literal: Person["name"].
T[keyof T] nghĩa là gì?
Nó lấy chỉ số T bằng union của tất cả các khóa, nên kết quả là union của tất cả kiểu thuộc tính. Với { a: string; b: number }, kết quả là string | number. Đây là cách nói ở cấp kiểu cho "một giá trị bất kỳ của object này".