Menu

Dấu chấm than trong TypeScript: non-null assertion (!)

Dấu chấm than sau một giá trị, như user!, là toán tử non-null assertion: nó bỏ null và undefined khỏi kiểu mà không kiểm tra gì lúc chạy. Tìm hiểu x! làm gì, các dạng definite assignment let x!: T và prop!: T, vì sao chúng rủi ro, và những cách an toàn hơ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.

Dấu chấm than sau một biểu thức, value!, là toán tử non-null assertion. Nó bỏ null và undefined khỏi kiểu, nên một number | undefined có thể được dùng như number. Đó là lời hứa với trình biên dịch, không phải phép kiểm tra: lúc chạy không có gì xảy ra.

Không có !, tea.toFixed(2) là lỗi TS18048, 'tea' is possibly 'undefined'. Có nó, code biên dịch được vì bạn đã báo cho trình biên dịch rằng khóa có tồn tại.

x! làm gì lúc chạy: không gì cả

Dấu ! bị xóa khỏi đầu ra. JavaScript được biên dịch từ prices.get("coffee")! chỉ là prices.get("coffee"). Nếu lời khẳng định sai, lỗi xuất hiện về sau, ở chỗ đầu tiên giá trị bị thiếu được dùng tới:

Chương trình in ra Cannot read properties of undefined (reading 'toFixed'). Lỗi xảy ra ở dòng ngay sau !, còn trong code thực tế nó có thể cách xa hơn nhiều: undefined có thể được lưu vào một đối tượng rồi gây lỗi ở một tệp khác. Khoảng cách đó là lý do ! rủi ro.

Definite assignment: let x!: T

Cùng ký hiệu đó trong một khai báo mang ý nghĩa liên quan. TypeScript theo dõi xem một biến có được gán trước khi đọc hay không, và nó không theo dõi được phép gán diễn ra bên trong một hàm khác:

index.ts(9,13): error TS2454: Variable 'config' is used before being assigned.

let config!: { port: number }; là một definite assignment assertion: "biến này sẽ được gán trước mọi lần đọc". Nó sửa được lỗi, nhưng có cùng nhược điểm như x!: nếu init() bị bỏ qua, lần đọc sẽ nhận undefined. Tổ chức lại code thường tốt hơn, ví dụ const config = init(); với init trả về đối tượng.

Thuộc tính của lớp: prop!: T

Với strictPropertyInitialization (một phần của strict), mọi thuộc tính của lớp phải được khởi tạo ngay trong khai báo hoặc trong hàm khởi tạo. Nếu không, bạn gặp lỗi TS2564: Property 'socket' has no initializer and is not definitely assigned in the constructor. Khi một thuộc tính được gán sau đó bởi một phương thức hay một framework, prop!: T bảo trình biên dịch chấp nhận nó:

console.log đầu tiên cho thấy khoảng trống: kiểu nói rằng socket luôn có mặt, nhưng trước open() nó là undefined. Gọi c.socket.send lúc đó vẫn biên dịch được và ném lỗi. Nếu thuộc tính thực sự có thể bị thiếu, hãy khai báo socket?: ... và kiểm tra nó, hoặc tạo đối tượng trong hàm khởi tạo. Nơi chính mà prop! là cách làm chuẩn là một framework điền thuộc tính sau khi khởi tạo: @ViewChild(...) child!: ChildDirective của Angular (được gán trước khi ngAfterViewInit chạy), hoặc các lớp entity của ORM mà thư viện điền các cột khi nạp một hàng (tài liệu của MikroORM viết @Property() title!: string).

Những cách an toàn hơn

Hầu hết các dấu ! có thể thay bằng thứ trình biên dịch kiểm tra được, hoặc bằng một phép kiểm tra báo lỗi rõ ràng đúng chỗ:

Cả hai hàm hỗ trợ đều thu hẹp kiểu giống !, nhưng một giả định sai sẽ tạo ra missing HOST ngay tại chỗ thay vì một TypeError ở đâu đó khác. assertDefined là một assertion function (asserts value is ...): sau lời gọi, trình biên dịch coi host là string. Xem thêm các mẫu ở trang type guard.

Thay vìHãy viếtĐiều xảy ra khi thiếu giá trị
user!.nameif (user) { user.name }khối lệnh bị bỏ qua
user!.nameuser?.nameundefined
count!count ?? 0giá trị mặc định được dùng
map.get(k)!must(map.get(k), "k")một lỗi rõ ràng ngay tại dòng đó
let x!: Tconst x = compute()không có gì để sai

Những dấu chấm than khác

! mang ý nghĩa khác nhau tùy vị trí:

CodeÝ nghĩa
value! (sau một biểu thức)non-null assertion, chỉ có trong TypeScript
let x!: T, prop!: Tdefinite assignment assertion, chỉ có trong TypeScript
!value (trước một biểu thức)phép NOT logic, JavaScript thuần
!!valuechuyển sang boolean, JavaScript thuần
a !== b, a != bso sánh khác, JavaScript thuần

Chỉ hai dạng đầu bị xóa lúc biên dịch. !value và !!value chạy lúc chạy và trả về một boolean.

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

Dấu chấm than sau một biến nghĩa là gì trong TypeScript?

value! là toán tử non-null assertion. Nó báo cho trình biên dịch rằng value không phải null hay undefined, nên kiểu của nó mất hai thành viên đó: string | undefined thành string. Nó bị xóa khỏi JavaScript đầu ra và không thêm phép kiểm tra nào lúc chạy, nên nếu bạn sai, chương trình sẽ lỗi về sau với một TypeError.

! và ? trong TypeScript khác nhau thế nào?

x! khẳng định giá trị có tồn tại và cho bạn kiểu không null, không kiểm tra gì cả. x?.y thì kiểm tra lúc chạy: nếu x là null hoặc undefined, nó dừng lại và trả về undefined. Trong khai báo, name?: string khiến thuộc tính thành tùy chọn, còn name!: string nói rằng một thuộc tính bắt buộc sẽ được gán ở đâu đó mà trình biên dịch không thấy.

let x!: string nghĩa là gì?

Đó là definite assignment assertion. Nó báo cho trình biên dịch rằng biến sẽ được gán trước khi được đọc, dù trình biên dịch không chứng minh được (ví dụ phép gán diễn ra bên trong một hàm khác). Không có nó, việc đọc biến là lỗi TS2454, Variable 'x' is used before being assigned.

Làm sao để sửa lỗi has no initializer and is not definitely assigned in the constructor?

Đó là lỗi TS2564 do strictPropertyInitialization. Hãy cho thuộc tính một giá trị ban đầu, gán nó trong hàm khởi tạo, khiến nó thành tùy chọn (prop?: T), hoặc, nếu một framework hay một phương thức khởi tạo thực sự gán nó trước khi dùng, hãy viết prop!: T. Dấu ! là lựa chọn cuối cùng vì không gì kiểm tra lời hứa đó.

Dùng toán tử non-null assertion có phải là thói quen xấu không?

Nó không sai, nhưng mỗi dấu ! là một lời khẳng định không được kiểm tra. Các cấu hình lint như @typescript-eslint/no-non-null-assertion sẽ cảnh báo nó. Hãy ưu tiên một phép kiểm tra có thu hẹp kiểu (if (x), x ?? fallback, x?.y) hoặc một hàm hỗ trợ ném ra lỗi rõ ràng. Chỉ giữ ! ở những chỗ giá trị được đảm bảo bởi logic mà trình biên dịch không theo dõi được.

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