Menu

Dictionary trong TypeScript: index signature, Record và Map

TypeScript không có class dictionary hay hashmap riêng; bạn gán kiểu cho một bảng tra cứu khóa và giá trị bằng index signature, Record<K, V> hoặc Map<K, V>. Tìm hiểu từng cách, cách kiểm tra một khóa có tồn tại không, cách thêm, xóa, duyệt, và vì sao noUncheckedIndexedAccess quan trọng với từ điển.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

TypeScript không có class dictionary hay hashmap riêng. Một từ điển (dictionary) hoặc là đối tượng thường được gán kiểu bằng index signature, { [key: string]: number }, cùng kiểu đó viết dưới dạng Record<string, number>, hoặc là một Map<string, number>. Cả ba đều lưu giá trị theo khóa; chúng khác nhau ở kiểu của khóa, cách gán kiểu cho khóa bị thiếu, và cách tuần tự hóa.

Với khóa dạng chuỗi và dữ liệu có dạng JSON, một đối tượng với Record<string, T> là lựa chọn thông thường. Với khóa không phải chuỗi, hoặc các phần tử liên tục được thêm và xóa, hãy dùng Map.

Index signature

Index signature, [key: KeyType]: ValueType, nói rằng "mọi khóa thuộc kiểu này đều ánh xạ đến một giá trị thuộc kiểu kia". Tên khóa (key, name, userId) chỉ để mô tả. Kiểu của khóa có thể là string, number, symbol, mẫu template literal, hoặc union của các kiểu này.

JavaScript chuyển khóa số thành chuỗi, nên một đối tượng { [id: number]: string } lúc chạy vẫn có khóa là chuỗi: Object.keys({ 1: "one" }) là [ '1' ]. Index signature dạng số chỉ giới hạn cách bạn truy cập nó trong TypeScript.

Record<K, V>

Record<string, V> là cách viết tắt của { [key: string]: V }. Khi dùng union các khóa literal thay cho string, nó trở thành một từ điển cố định phải chứa đủ mọi khóa:

Bỏ sót staging trong urls là lỗi biên dịch TS2741 (Property 'staging' is missing...), nhờ vậy Record với khóa là union trở thành một bảng tra cứu được kiểm tra. Partial biến mỗi giá trị thành number | undefined.

Map làm hash map

Map nhận khóa thuộc bất kỳ kiểu nào, giữ thứ tự thêm vào, có size, và gán kiểu trung thực cho khóa bị thiếu: get trả về V | undefined.

Kiểm tra khóa có tồn tại không

Có nhiều cách kiểm tra, và chúng không hoàn toàn mang cùng một ý nghĩa:

Cách kiểm traDùng choCần lưu ý
Object.hasOwn(obj, key)đối tượngES2022; với target cũ hơn, dùng Object.prototype.hasOwnProperty.call(obj, key)
key in objđối tượngCũng trả về true với khóa kế thừa như toString và constructor
obj[key] !== undefinedđối tượngKhông phân biệt được khóa bị thiếu với khóa lưu giá trị undefined
if (obj[key])đối tượngCũng ra false với giá trị 0, "" và false
map.has(key)MapKhông thu hẹp kiểu cho map.get(key) phía sau
map.get(key) !== undefinedMapCùng lưu ý về undefined như với đối tượng

Vấn đề khóa kế thừa là lý do các khóa do người dùng nhập như "constructor" hay "__proto__" khiến đối tượng thường trở nên rủi ro khi dùng làm từ điển. Map không có những khóa như vậy.

Vấn đề kiểu của khóa bị thiếu

Trên index signature hoặc Record<string, T>, đọc bất kỳ khóa nào cũng có kiểu T, kể cả khóa không tồn tại. Trình biên dịch cho phép bạn gọi phương thức trên một giá trị mà lúc chạy là undefined:

Tùy chọn biên dịch noUncheckedIndexedAccess khắc phục điều này: khi bật, colors["grass"] có kiểu string | undefined và lời gọi toUpperCase là lỗi biên dịch cho đến khi bạn kiểm tra. Tùy chọn này không thuộc strict, nên phải bật riêng trong tsconfig.json; xem strict mode để biết các cờ đi kèm khác. Map không có lỗ hổng này, vì get luôn bao gồm undefined.

Thêm, xóa và duyệt

delete dùng được với thuộc tính của index signature. Với một thuộc tính có tên bắt buộc của kiểu đối tượng, nó là lỗi biên dịch TS2790, The operand of a 'delete' operator must be optional.

Nên dùng cách nào

Nhu cầuDùng
Khóa dạng chuỗi, đọc vào hoặc xuất ra JSONRecord<string, T>
Một tập khóa cố định đã biết trước, tất cả đều bắt buộcRecord<"a" | "b", T>
Thuộc tính có tên cộng thêm các khóa tùy ýkiểu đối tượng có index signature
Khóa là đối tượng, là số muốn giữ nguyên dạng số, hoặc bất kỳ thứ gì không phải chuỗiMap<K, V>
Thường xuyên thêm và xóa, hoặc hay đọc kích thướcMap<K, V>
Khóa đến từ người dùngMap<K, V> (không có khóa kế thừa)

Câu hỏi thường gặp

Làm sao tạo một dictionary trong TypeScript?

Gán kiểu cho một đối tượng thường bằng index signature, const ages: { [name: string]: number } = {}, hoặc kiểu tương đương Record<string, number>. Sau đó thêm phần tử bằng ages["ada"] = 36. Với khóa không phải chuỗi, hoặc một tập hợp thường xuyên thêm và xóa, hãy dùng new Map<string, number>().

TypeScript có HashMap không?

Không có dưới cái tên đó. Map có sẵn của JavaScript chính là một hash map: Map<K, V> lưu các cặp khóa và giá trị với tốc độ tra cứu nhanh theo khóa, giữ thứ tự thêm vào, và nhận khóa thuộc bất kỳ kiểu nào. Một đối tượng thường có kiểu Record<string, V> là lựa chọn phổ biến còn lại cho khóa dạng chuỗi.

Làm sao kiểm tra một khóa có tồn tại trong dictionary TypeScript?

Với dictionary là đối tượng, dùng Object.hasOwn(dict, key) hoặc key in dict (cách này còn thấy cả thuộc tính kế thừa như toString), hoặc đọc giá trị rồi so sánh với undefined. Với Map, dùng map.has(key), hoặc kiểm tra trực tiếp kết quả của map.get(key), vì has không thu hẹp kiểu cho lời gọi get phía sau.

Index signature và Record khác nhau thế nào?

{ [key: string]: T } và Record<string, T> mô tả cùng một kiểu. Record ngắn gọn hơn và còn nhận được union các khóa cụ thể, Record<"a" | "b", T>, khi đó mọi khóa đều bắt buộc. Index signature có thể kết hợp với các thuộc tính có tên trong cùng một kiểu đối tượng, và khóa của nó có thể mang một cái tên giúp mô tả ý nghĩa, như { [userId: string]: User }.

Vì sao đọc một khóa không có trong dictionary lại không báo lỗi?

Mặc định, dict[key] trên index signature hoặc Record<string, T> có kiểu T, dù lúc chạy giá trị là undefined với khóa không tồn tại. Bật noUncheckedIndexedAccess trong tsconfig.json thì kiểu trở thành T | undefined, buộc bạn phải kiểm tra. strict không bao gồm tùy chọn này.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu