typeof có hai nghĩa trong TypeScript. Trong một biểu thức, nó là toán tử JavaScript: nó chạy, trả về một chuỗi như "string" hay "number", và TypeScript thu hẹp kiểu của biến khi bạn so sánh chuỗi đó. Ở vị trí kiểu, nó là một truy vấn kiểu: typeof config cho ra kiểu tĩnh của biến config.
Dòng cuối cho thấy sự khác biệt: typeof custom trong console.log là toán tử lúc chạy và in ra object, còn typeof defaults sau type Options = là truy vấn lúc biên dịch và không bao giờ có mặt trong JavaScript.
Hai nghĩa trong nháy mắt
typeof lúc chạy (JavaScript) | typeof truy vấn kiểu (TypeScript) | |
|---|---|---|
| Xuất hiện ở đâu | Mọi biểu thức: if, return, console.log(...) | Vị trí kiểu: sau :, trong type X = ..., bên trong <...> |
| Ví dụ | typeof x === "string" | let y: typeof x; |
| Được tính khi nào | Khi code chạy | Bởi trình biên dịch, rồi bị xóa |
| Kết quả | Một trong tám chuỗi | Một kiểu |
| Dùng để | Kiểm tra một giá trị và thu hẹp kiểu của nó | Tạo ra một kiểu từ một giá trị |
TypeScript phân biệt hai nghĩa dựa vào vị trí, nên cùng một từ khóa không bao giờ mang cả hai nghĩa cùng lúc.
Kết quả của typeof lúc chạy
Toán tử lúc chạy trả về một trong tám chuỗi. Chạy đoạn code này để xem các giá trị thường gặp cho ra gì:
| Giá trị | Kết quả typeof | Ghi chú |
|---|---|---|
"hi", template string | "string" | |
42, 3.14, NaN, Infinity | "number" | NaN là một số; kiểm tra nó bằng Number.isNaN |
10n | "bigint" | |
true, false | "boolean" | |
undefined | "undefined" | |
Symbol("id") | "symbol" | |
| hàm, arrow function, class | "function" | class là một hàm khởi tạo |
null | "object" | một bug lịch sử của JavaScript |
{}, [], new Date(), new Map() | "object" | mảng là đối tượng: dùng Array.isArray |
new String("x"), new Number(1) | "object" | đối tượng bọc, nên tránh |
Trong JavaScript thuần, typeof trên một biến chưa từng được khai báo trả về "undefined" thay vì ném lỗi. Trong TypeScript, code đó không biên dịch được: Cannot find name 'notDeclared'. (TS2304).
Thu hẹp kiểu với typeof
Một phép so sánh typeof trong if, switch, toán tử ba ngôi hay sau && sẽ thu hẹp kiểu của biến trong từng nhánh. Đây là cách chuẩn để xử lý một union các kiểu nguyên thủy, và là phép kiểm tra đầu tiên nên dùng với unknown.
typeof x !== "string" thu hẹp theo chiều ngược lại, thành mọi thứ trừ string. Và trình biên dịch biết tám chuỗi có thể có, nên một lỗi gõ sai trong phép so sánh là lỗi biên dịch, thay vì một phép kiểm tra âm thầm luôn sai:
Trình biên dịch in ra index.ts(3,7): error TS2367: This comparison appears to be unintentional because the types '"bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"' and '"strnig"' have no overlap. Trong JavaScript thuần, lỗi gõ đó sẽ âm thầm đẩy mọi chuỗi vào nhánh number. Sửa lại chính tả thì code in ra text.
Cái bẫy typeof null
Vì typeof null là "object", TypeScript thu hẹp typeof x === "object" thành object | null, không phải object. Hãy thêm một phép kiểm tra null, nếu không việc đọc thuộc tính sẽ là lỗi (TS18047 'x' is possibly 'null'.).
typeof không phân biệt được mảng, ngày tháng, map và đối tượng thường: tất cả đều là "object". Dùng Array.isArray(x) cho mảng và x instanceof Date cho instance của class. Cả typeof lẫn bất kỳ thứ gì khác đều không kiểm tra được một giá trị có khớp với interface lúc chạy hay không, vì interface bị xóa khi biên dịch; để làm việc đó, hãy viết một hàm type guard kiểm tra các thuộc tính.
typeof ở vị trí kiểu
Trong một kiểu, typeof someVariable sao chép kiểu mà TypeScript đã suy ra (hoặc bạn đã khai báo) cho biến đó. Nó giúp bạn không phải tự viết kiểu cho một giá trị đã có, và giữ hai thứ luôn khớp nhau.
const theme = {
primary: "#3178c6",
spacing: 8,
dark: false,
};
type Theme = typeof theme;
// { primary: string; spacing: number; dark: boolean }
function withSpacing(t: Theme, factor: number): Theme {
return { ...t, spacing: t.spacing * factor };
}
console.log(withSpacing(theme, 2)); // { primary: '#3178c6', spacing: 16, dark: false }
let userName = "Ada";
const fixed = "Ada";
type A = typeof userName; // string
type B = typeof fixed; // "Ada" (a const keeps its literal type)
Kết quả phụ thuộc vào cách khai báo biến: một let hoặc một thuộc tính của đối tượng bị mở rộng kiểu (string, number), còn một const kiểu nguyên thủy giữ literal type của nó. Thêm as const vào một đối tượng hoặc mảng thì typeof cho ra literal type readonly ở mọi cấp. typeof cũng nhận truy cập thuộc tính, typeof theme.spacing là number, nhưng không nhận biểu thức tùy ý: type T = typeof getTheme(); là lỗi cú pháp (';' expected., TS1005). Để lấy kiểu mà một lời gọi trả về, hãy dùng ReturnType<typeof getTheme>.
keyof typeof: khóa của đối tượng thành kiểu
keyof typeof obj là cách kết hợp phổ biến nhất. typeof biến đối tượng thành một kiểu, keyof lấy các khóa của nó, và bạn có một union các tên thuộc tính luôn đúng khi đối tượng thay đổi.
Cùng mẹo đó trên một mảng hằng cho ra union các giá trị của nó: với const roles = ["admin", "editor"] as const, kiểu (typeof roles)[number] là "admin" | "editor". Không có as const, nó chỉ là string. Trang keyof trình bày riêng về keyof.
ReturnType<typeof fn> và typeof trên class
Các utility type như ReturnType và Parameters nhận một kiểu hàm. Tên hàm là một giá trị, nên hãy đưa nó qua typeof trước:
Viết ReturnType<createUser> sẽ báo lỗi 'createUser' refers to a value, but is being used as a type here. Did you mean 'typeof createUser'? (TS2749), và đó chính là trình biên dịch đang yêu cầu đúng điều này.
Class là trường hợp duy nhất mà một tên vừa là giá trị vừa là kiểu. User khi dùng làm kiểu nghĩa là một instance của class; typeof User nghĩa là chính class đó, tức hàm khởi tạo, kể cả các thành viên static:
class Point {
static origin = new Point(0, 0);
constructor(public x: number, public y: number) {}
}
const p: Point = new Point(1, 2); // an instance
const Ctor: typeof Point = Point; // the class (constructor + statics)
const q = new Ctor(3, 4); // q: Point
type Instance = InstanceType<typeof Point>; // Point
typeof, instanceof và type guard
| Phép kiểm tra | Dùng cho | Ví dụ |
|---|---|---|
typeof | kiểu nguyên thủy, hàm, "có phải đối tượng không" | typeof x === "number" |
Array.isArray | mảng | Array.isArray(x) |
instanceof | instance của class (Date, Error, class của bạn) | x instanceof Date |
in | union đối tượng, theo thuộc tính | "email" in x |
| Hàm type guard | interface, type alias, mọi thứ khác | isUser(x) với x is User |
Hãy chọn công cụ hẹp nhất phù hợp. Với instance của class, xem trang instanceof; với cấu trúc được mô tả bằng interface, hàm type guard là phép kiểm tra lúc chạy duy nhất có thể dùng.
Câu hỏi thường gặp
typeof làm gì trong TypeScript?
Hai việc khác nhau tùy vào vị trí xuất hiện. Trong một biểu thức, typeof x là toán tử JavaScript: nó chạy và trả về một chuỗi như "string" hay "object", và TypeScript thu hẹp kiểu của x khi bạn so sánh chuỗi đó. Trong một chú thích kiểu, typeof x là một truy vấn kiểu: trình biên dịch tính nó ra kiểu tĩnh của biến x, và nó biến mất khỏi JavaScript xuất ra.
keyof typeof trong TypeScript là gì?
keyof typeof obj cho ra union các tên thuộc tính của một đối tượng dưới dạng string literal type. typeof obj biến giá trị đối tượng thành kiểu của nó, và keyof lấy các khóa của kiểu đó. Với const colors = { red: "#f00", blue: "#00f" }, keyof typeof colors là "red" | "blue".
Vì sao typeof null trả về "object"?
Đó là một bug từ phiên bản đầu tiên của JavaScript, giờ không thể sửa được nữa nếu không muốn làm hỏng các trang web. TypeScript mô phỏng đúng điều này: sau typeof x === "object", x được thu hẹp thành object | null, nên bạn còn phải kiểm tra x !== null trước khi đọc thuộc tính.
Làm sao kiểm tra một giá trị có phải mảng trong TypeScript không?
Dùng Array.isArray(value), hàm này thu hẹp về kiểu mảng. typeof không làm được việc này: typeof [] là "object", giống như với đối tượng thường và null.
typeof và instanceof trong TypeScript khác nhau thế nào?
typeof kiểm tra nhóm kiểu nguyên thủy của một giá trị ("string", "number", "function", "object"...). instanceof kiểm tra xem một đối tượng có được tạo bởi một class hay hàm khởi tạo cụ thể không, như Date hay class User của bạn. Dùng typeof cho kiểu nguyên thủy và instanceof cho instance của class; cả hai đều không kiểm tra được interface hay type alias, vì chúng không tồn tại lúc chạy.