Menu

null và undefined trong TypeScript: kiểm tra, ?. và ??

Khi bật strictNullChecks, null và undefined là các kiểu riêng mà TypeScript buộc bạn phải xử lý. Tìm hiểu cách kiểm tra chúng, optional chaining (?.), dấu hai chấm hỏi (??) và ??=, cùng sự khác nhau giữa thuộc tính tùy chọn và | undefined.

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.

Khi bật strictNullChecks (một phần của strict), null và undefined là các kiểu riêng. Một string không bao giờ có thể là null; giá trị có thể bị thiếu phải nói rõ điều đó trong kiểu của nó, ví dụ string | null, và TypeScript buộc bạn xử lý trường hợp đó trước khi dùng giá trị.

Sau phép kiểm tra, trình biên dịch biết name là string, nên được phép gọi .split. Phần còn lại của trang này trình bày các cách kiểm tra như vậy và những toán tử giúp viết chúng ngắn gọn.

strictNullChecks và lỗi "possibly null"

Khi một kiểu có chứa null hoặc undefined, TypeScript không cho bạn dùng giá trị như thể nó luôn tồn tại:

index.ts(3,22): error TS18047: 'name' is possibly 'null'.

Phiên bản với undefined là lỗi TS18048, 'x' is possibly 'undefined'. Nếu không có strictNullChecks, null và undefined được chấp nhận trong mọi kiểu và đoạn code này vẫn biên dịch, rồi bị crash lúc chạy với một TypeError ngay lần đầu name là null. Loại bug này chính là lý do chính để luôn bật strict.

Những kiểu này xuất hiện ở đâu trong code hằng ngày:

NguồnKiểu
arr.find(...)T | undefined
map.get(key)V | undefined
thuộc tính tùy chọn p?: TT | undefined khi đọc
tham số tùy chọn x?: TT | undefined bên trong hàm
str.match(re)RegExpMatchArray | null
JSON.parse(text)any, nên không có gì được kiểm tra

Kiểm tra null và undefined

Mỗi phép kiểm tra dưới đây đều thu hẹp kiểu bên trong khối lệnh. Chọn cách khớp với thứ bạn muốn loại ra.

Phép kiểm traLoại khỏi kiểu
x !== undefinedundefined
x !== nullnull
x != nullnull và undefined
typeof x !== "undefined"undefined
if (x)null và undefined, đồng thời bỏ qua các giá trị 0, "", false, NaN

== null là chỗ duy nhất mà so sánh lỏng được xem là cách viết chuẩn: nó đúng với đúng null và undefined, không gì khác. Kiểm tra truthy sẽ coi chuỗi rỗng là bị thiếu, và đó thường là một bug.

Optional chaining: ?.

a?.b đọc b nếu a không phải null hay undefined, ngược lại thì dừng và trả về undefined. Toán tử này cũng dùng được với chỉ số, a?.[i], và với lời gọi hàm, fn?.().

Kiểu của bob.address?.city là string | undefined: optional chaining thêm undefined vào kết quả, nên bạn thường dùng nó kèm ??. Hành vi lúc chạy là JavaScript thuần; xem optional chaining để biết chi tiết về cơ chế ngắt sớm (short-circuit).

Dấu hai chấm hỏi: ??

a ?? b trả về a, trừ khi nó là null hoặc undefined, khi đó trả về b. Toán tử này thay cho cách viết cũ a || b, vốn loại bỏ luôn cả 0, "", false và NaN:

Giá trị vế tráileft || "d"left ?? "d"
null"d""d"
undefined"d""d"
0"d"0
"""d"""
false"d"false
NaN"d"NaN

Về mặt kiểu, ?? loại null và undefined khỏi vế trái rồi hợp phần còn lại với vế phải, vì vậy scores.get("Linus") ?? 0 gán được cho một number.

Gán nullish: ??=

a ??= b gán b cho a chỉ khi a là null hoặc undefined. Hai toán tử anh em ||= và &&= gán khi vế trái là falsy hoặc truthy.

retries: 0 được giữ nguyên qua ??=, còn label rỗng bị ||= thay thế. Sau opts.retries ??= 3, TypeScript thu hẹp opts.retries thành number trong phần còn lại của hàm.

Thuộc tính tùy chọn và | undefined

nickname?: string và nickname: string | undefined khi đọc thì giống nhau, nhưng khác ở chỗ khóa có bắt buộc phải tồn tại hay không:

Dùng ? khi nơi gọi có thể bỏ qua thuộc tính, và | undefined khi bạn muốn mọi nơi gọi đều phải truyền nó một cách tường minh, kể cả khi giá trị là undefined. Tùy chọn exactOptionalPropertyTypes (không thuộc strict) siết ? chặt hơn nữa: lúc đó nickname?: string từ chối { nickname: undefined } và chỉ chấp nhận khóa vắng mặt hoặc một chuỗi. Tham số tùy chọn của hàm (x?: number) hoạt động giống thuộc tính tùy chọn: bên trong hàm, x có kiểu number | undefined.

Nên dùng null hay undefined

TypeScript không bắt bạn phải chọn, nhưng trộn cả hai trong một dự án nghĩa là mọi phép kiểm tra đều phải xử lý hai trường hợp. Một quy ước phổ biến:

  • Dùng undefined (và thuộc tính tùy chọn) cho trạng thái "chưa đặt" trong các kiểu của bạn. Đây là thứ JavaScript tự tạo ra theo mặc định: thuộc tính không tồn tại, đối số bị bỏ qua, find và Map.get không tìm thấy.
  • Chấp nhận null ở nơi API trả về nó: JSON không có undefined, String.prototype.match và nhiều phương thức DOM trả về null.
  • Kiểm tra bằng == null khi giá trị có thể là một trong hai.

Truy cập mảng theo chỉ số là kẽ hở duy nhất: users[5] có kiểu của phần tử ngay cả khi chỉ số vượt ngoài phạm vi. Tùy chọn noUncheckedIndexedAccess (không thuộc strict) thêm | undefined vào mọi lần truy cập theo chỉ số để trình biên dịch bắt được cả trường hợp đó.

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

Dấu hai chấm hỏi trong TypeScript có nghĩa là gì?

a ?? b là toán tử nullish coalescing của JavaScript. Nó trả về a, trừ khi a là null hoặc undefined, khi đó nó trả về b. Khác với ||, toán tử này giữ lại các giá trị falsy khác như 0, "" và false. TypeScript loại null và undefined khỏi kiểu của vế trái, nên khi a có kiểu string | undefined thì a ?? "x" có kiểu string.

Làm sao kiểm tra một giá trị có phải undefined trong TypeScript?

So sánh trực tiếp: if (value !== undefined) { ... }. TypeScript thu hẹp kiểu bên trong khối lệnh đó. Để loại cả null lẫn undefined trong một lần kiểm tra, dùng value != null (so sánh lỏng), đây là chỗ duy nhất mà ==/!= được xem là cách viết chuẩn. Kiểm tra truthy (if (value)) cũng thu hẹp kiểu nhưng bỏ qua luôn 0, "" và false.

Lỗi "Object is possibly undefined" nghĩa là gì?

Các lỗi TS18048 ('x' is possibly 'undefined') và TS18047 ('x' is possibly 'null'), hoặc TS2532 (Object is possibly 'undefined') khi giá trị không có tên đơn giản như trong getUser().address.city, đều đến từ strictNullChecks: kiểu có chứa undefined hoặc null, nhưng code lại dùng giá trị như thể nó không thể thiếu. Hãy kiểm tra trước, dùng optional chaining (x?.name), đặt giá trị mặc định bằng ??, hoặc sửa kiểu nếu giá trị thật sự không bao giờ thiếu.

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

Đây là hai kiểu riêng, mỗi kiểu chỉ có một giá trị. JavaScript dùng undefined cho những thứ chưa từng được gán (thuộc tính không tồn tại, đối số bị bỏ qua, Map.get với khóa không có) còn các API dùng null để chủ ý nói "không có giá trị" (JSON, nhiều phương thức DOM). TypeScript theo dõi hai kiểu này riêng biệt, nên string | null không nhận undefined. Nhiều dự án chọn undefined cho code của mình và chỉ chấp nhận null ở ranh giới với bên ngoài.

Thuộc tính tùy chọn có giống | undefined không?

Không hẳn. name?: string nghĩa là thuộc tính có thể vắng mặt hoàn toàn, và khi đọc sẽ nhận string | undefined. name: string | undefined nghĩa là thuộc tính bắt buộc phải có, dù giá trị của nó là undefined. Khi bật exactOptionalPropertyTypes, name?: string còn không nhận giá trị undefined được gán tường minh.

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