TypeScript - định nghĩa kiểu cho index của Object - indexable-types hay cách gọi khác là index signature 🖊️
Trong bào viết sử dụng source-code TypeScript, do đó cách nhanh nhất có thể chạy thử chúng là copy và dán vào web typescript playground.
Tại sao cần nó?
Ta xem ví dụ khi muốn lưu trữ data về tần suất đọc sách của bản thân theo ngày vào 1 object để tiện truy cập. Ví dụ hôm nay 2022-01-29 mình đọc sách tức là:
const readingBookDates: any = {};
readingBookDates['2022-01-29'] = true;
Nếu ta không sử dụng type là any
như trên thì sẽ có lỗi:
Element implicitly has an 'any' type because expression of type '"2022-01-29"' can't be used to index type '{}'. Property '2022-01-29' does not exist on type '{}'.(7053)
Liệu có cách nào không phải any
, để tránh các tai nạn khi gán nhầm kiểu cho giá trị cho index - 2022-01-29 cho 1 object động như thế này? Ví dụ ta vẫn có thể viết như sau mà không phát hiện lỗi về kiểu:
readingBookDates['2022-01-29'] = false;
readingBookDates['2022-01-29'] = 1;
readingBookDates[1] = 0;
Lúc này ta muốn tạo ra 1 object có các properties hay index là 1 loại string unique mô tả thông tin năm tháng ngày, tuy nhiên những index hay properties là động, không để set cứng như object type, interface thông thường.
Những bài toán về mapping giữa key - value kiểu từ điển thường sẽ gặp những kiểu như thế này. Đó là lúc ta cần đến indexable-types.
Cách dùng cơ bản
Thay vì để kiểu any, ta sẽ định nghĩa kiểu như sau:
const readingBookDates: { [key: string]: true} = {};
readingBookDates['2022-01-29'] = true;
Những câu gán giá trị như sau sẽ báo lỗi hết:
readingBookDates['2022-01-29'] = false;
readingBookDates['2022-01-29'] = 1;
readingBookDates[1] = 0;
Giờ ta chỉ có thể gán cho những index đúng chuẩn là string
với giá trị chỉ có thể là true
.
Cú pháp là ngoặc vuông []
bao ngoài, key
đại diện cho index, tiếp theo là kiểu của nó. Cả cụm []
tương đương với 1 thuộc tính của kiểu thông thường.
Ta hoàn toàn có thể thay thế tên gọi đại diện key
thành bất kỳ từ khóa nào mà ta muốn. Thật vậy:
const readingBookDates: { [index: string]: true} = {};
readingBookDates['2022-01-29'] = true;
Tôi thì hay dùng key
vì nó dễ hiểu và trên internet hay thấy từ khóa đó được sử dụng.
Ta cũng có thể sử dụng nó để định nghĩa kiểu cho 1 dãy chỉ có thể là các cái tên:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
Vì bản chất array cũng là 1 object đặc biệt với index là kiểu số mà.
Các chú ý khi sử dụng
Để dễ phân biệt giữa kiểu của index signatures ([key: string]
- là cái string này này) với kiểu của data được mapping tới ([key: string]: number
- là cái number này này), ta sẽ chúng lần lượt là kiểu index và kiểu trả về.
Các kiểu index
Không chỉ có string và number, ta có tất cả 4 kiểu index:
- string
- number
- symbol
- template strings
Ở bài này mình sẽ không đi sâu vào symbol và template strings.
Ví dụ về dùng nhiều loại kiểu index cho cùng 1 interface:
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
interface Okay {
[x: number]: Dog;
[x: string]: Animal;
}
Tuy nhiên, chú ý là do kiểu number thực chất được JS convert sang string nên bắt buộc kiểu trả về của number phải là tập con của kiểu trả về của string.
Hơi phức tạp nhỉ, theo ví dụ trên thì:
- tập cha (string index) chứa tập con (number index)
- tập cha (Animal) chứa tập con (Dog)
(1) và (2) phải đi đôi với nhau.
Ngược lại thì sẽ báo lỗi:
interface NotOkay {
[x: number]: Animal; // error
[x: string]: Dog;
}
Kết hợp với các index có tên riêng thông thường
interface NumberDictionary {
[index: string]: number;
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
}
Tương tự logic tập con loằng ngoằng phần trên, nếu định index có tên riêng thông thường như length, name thì ta không thể set kiểu string cho name và set được number cho length vì:
- tập cha (string index) chứa tập con (length - chỉ là 1 string)
- tập cha (number) chứa tập con (number) (:D)
Tạo ra tập cha của number và string đó là phép hoặc: number | string
thì viết như dưới đây ok:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Tính năng index signatures readonly
Ta cũng có thể dùng tính năng cấm gán lại giá trị khác - readonly cho tất cả các index:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[1] = "Mallory"; // error!
myArray[2] = "Mallory"; // error!
Sử dụng với kiểu string có dạng đặc trưng riêng
interface HeadersResponse {
"content-type": string,
date: string,
"content-length": string
// Permit any property starting with 'data-'.
[headerName: `x-${string}`]: string;
}
function handleResponse(r: HeadersResponse) {
// Handle known, and x- prefixed
const type = r["content-type"]
const poweredBy = r["x-powered-by"]
// Unknown keys without the prefix raise errors
const origin = r.origin
// Error: Property 'origin' does not exist on type 'HeadersResponse'.
}
Kiểu HeadersResponse
cho phép các thuộc tính có chữ cái đầu là x, còn origin property thì không cho phép.