TypeScript - định nghĩa kiểu cho index của Object - indexable-types hay cách gọi khác là index signature 🖊️

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.

surprised

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 indexkiểu trả về.

Các kiểu index

Không chỉ có stringnumber, 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 symboltemplate 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ì:

  1. tập cha (string index) chứa tập con (number index)
  2. 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;
}

thinking

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ì:

  1. tập cha (string index) chứa tập con (length - chỉ là 1 string)
  2. 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.

Tham khảo


Photo by Joe Pohle on Unsplash