Menu

keyof trong TypeScript: lấy khóa của đối tượng thành kiểu

keyof nhận một kiểu đối tượng và trả về union các tên thuộc tính của nó. Tìm hiểu keyof với interface, keyof typeof cho đối tượng thường, truy cập thuộc tính có kiểu bằng generic, index signature (string | number), và vì sao Object.keys trả về string[].

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.

keyof nhận một kiểu đối tượng và tạo ra union các tên thuộc tính của nó. keyof User dưới đây là kiểu "id" | "name" | "email", nên một biến có kiểu đó chỉ giữ được một trong ba chuỗi ấy.

keyof chỉ tồn tại trong hệ thống kiểu. Nó bị xóa khi code được biên dịch, nên không bao giờ kiểm tra gì lúc chạy. Thứ nó mang lại là sự đảm bảo lúc biên dịch rằng một khóa là tên của một thuộc tính có thật: đổi tên email trong interface, và mọi "email" có kiểu keyof User đều thành lỗi.

keyof typeof: khóa của một đối tượng thường

keyof hoạt động trên kiểu, không phải giá trị. Với một đối tượng bạn viết dưới dạng giá trị, trước hết lấy kiểu của nó bằng typeof, rồi mới lấy các khóa:

Đọc từ trong ra ngoài: typeof colors là { red: string; green: string; blue: string }, và keyof của kiểu đó là union các khóa. Viết keyof colors mà không có typeof sẽ báo error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'?.

Đây là cách thông dụng để suy ra một union từ bảng tra cứu: thêm một màu vào đối tượng và kiểu Color tự lớn theo. typeof còn có ý nghĩa thứ hai lúc chạy; trang typeof trình bày cả hai.

Truy cập thuộc tính có kiểu bằng generic

Cách dùng keyof phổ biến nhất là làm ràng buộc generic. K extends keyof T nghĩa là "K là một trong các khóa của T", và T[K] là kiểu của thuộc tính tại khóa đó:

Type parameter thứ hai rất quan trọng. Với key: keyof T và không có K, kiểu trả về là T[keyof T], tức union của mọi kiểu thuộc tính (ở đây là string | number | boolean), nên title.toUpperCase() sẽ báo error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean'. Giữ đúng khóa trong K giúp giữ đúng kiểu thuộc tính. T[K] là một indexed access type, được mô tả ở trang indexed access types.

Truy cập bằng một string thông thường

Một lỗi rất hay gặp xuất hiện khi bạn truy cập đối tượng bằng một string đến từ nơi khác:

Trình biên dịch in ra:

index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
  No index signature with a parameter of type 'string' was found on type 'Settings'.

Bất kỳ chuỗi nào cũng có thể được truyền vào read, kể cả "colour", và Settings không nói gì về khóa đó. Có hai cách sửa: chỉ nhận các khóa có thật, hoặc kiểm tra chuỗi chưa rõ trước khi dùng.

name is keyof Settings là một type predicate: khi hàm trả về true, TypeScript coi input là một khóa bên trong khối if. Trang type guard giải thích chi tiết về predicate.

keyof với index signature: string | number

Với kiểu có index signature, keyof trả về kiểu của chỉ số, và string index signature cho ra string | number, không chỉ string:

JavaScript chuyển mọi khóa thuộc tính dạng số thành chuỗi, nên scores[42] và scores["42"] là cùng một thuộc tính. TypeScript mô hình hóa điều đó bằng cách thêm number. Đầu ra còn cho thấy một quy tắc của JavaScript: các khóa dạng số nguyên được liệt kê trước, theo thứ tự tăng dần, rồi mới đến các khóa chuỗi khác. Khi một hàm generic chỉ cần khóa dạng chuỗi, hãy viết Extract<keyof T, string> hoặc string & keyof T; Exclude và Extract có trang riêng.

Tóm tắt kết quả của keyof

Kiểukeyof cho ra
{ id: number; name: string }"id" | "name"
{ [key: string]: number }string | number
{ [index: number]: string }number
{}never
anystring | number | symbol
{ a: 1; b: 2 } | { a: 3; c: 4 }"a" (khóa mà mọi thành viên đều có)
{ a: 1 } & { c: 4 }"a" | "c" (khóa của bất kỳ bên nào)
string[]number cùng tên mọi phương thức của mảng ("length", "push", ...)

Hai dòng union và intersection thoạt nhìn có vẻ ngược. Một giá trị kiểu union có thể là bất kỳ thành viên nào, nên chỉ những khóa có ở cả hai mới an toàn. Một giá trị kiểu intersection có mọi thứ của cả hai, nên nó có tất cả các khóa.

Vì sao Object.keys trả về string[]

Object.keys(obj) có kiểu string[], không bao giờ là (keyof T)[]. Lý do là structural typing: một giá trị có thể mang nhiều thuộc tính hơn kiểu đã khai báo.

p có kiểu Point, nhưng lúc chạy nó có khóa z. Nếu Object.keys hứa trả về ("x" | "y")[], code dựa vào điều đó sẽ sai với p. Phép ép kiểu an toàn với các object literal do chính bạn tạo; với đối tượng đến từ bên ngoài, hãy giữ khóa là string và kiểm tra chúng.

keyof trong mapped type

keyof cũng là cách để lặp qua các khóa của một kiểu nhằm tạo kiểu mới. Các utility type có sẵn như Partial và Readonly được viết theo cách này:

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

interface User {
  id: number;
  name: string;
}

type UserDraft = MyPartial<User>; // { id?: number; name?: string }

[K in keyof T] duyệt từng khóa, và T[K] đọc kiểu thuộc tính tại khóa đó. Mapped type có trang riêng, kèm modifier và cách đổi tên khóa.

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

keyof làm gì trong TypeScript?

keyof T tạo ra union các tên thuộc tính của kiểu T. Với interface User { id: number; name: string }, keyof User là "id" | "name". Nó chỉ hoạt động trên kiểu, lúc biên dịch, và không sinh ra JavaScript nào.

keyof typeof trong TypeScript là gì?

keyof cần một kiểu, nhưng một đối tượng thường như const colors = { red: "#f00" } lại là giá trị. typeof colors biến giá trị thành kiểu của nó, rồi keyof typeof colors cho ra union các khóa, ở đây là "red". Viết keyof colors sẽ gặp lỗi TS2749.

Vì sao keyof trả về string | number?

Khi kiểu có string index signature như { [key: string]: number }. JavaScript chuyển khóa thuộc tính dạng số thành chuỗi, nên obj[42] cũng là cách truy cập hợp lệ, và TypeScript đưa number vào kiểu khóa. Dùng Extract<keyof T, string> hoặc string & keyof T nếu bạn chỉ muốn các khóa dạng chuỗi.

Vì sao Object.keys trả về string[] thay vì (keyof T)[]?

Vì một đối tượng có thể chứa nhiều thuộc tính hơn kiểu của nó liệt kê: một giá trị có trường thừa vẫn gán được cho kiểu hẹp hơn. Khai báo kết quả là (keyof T)[] sẽ sai với những đối tượng đó. Nếu bạn biết chắc đối tượng chỉ có đúng các khóa đã khai báo, hãy ép kiểu: Object.keys(obj) as (keyof typeof obj)[].

Làm sao để sửa lỗi "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type"?

Chỉ số là một string thông thường, còn kiểu đối tượng không có string index signature. Hãy khai báo tham số là keyof YourType thay vì string, hoặc kiểm tra chuỗi trước bằng một type guard thu hẹp nó thành keyof YourType, hoặc cho đối tượng một index signature hay kiểu Record<string, V> nếu nó thật sự nhận mọi khóa.

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