Menu

Kiểu never trong TypeScript: kiểm tra đầy đủ, never và void

never là kiểu không có giá trị nào. Đó là kiểu trả về của các hàm không bao giờ kết thúc, là kiểu còn lại khi việc thu hẹp đã loại hết mọi trường hợp, và là công cụ đằng sau phép kiểm tra switch đầy đủ. Tìm hiểu never đến từ đâu và khác void thế nào.

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.

never là kiểu không có giá trị nào. Một hàm có kiểu trả về never không bao giờ trả về bình thường: nó ném lỗi hoặc chạy mãi mãi. Một biến kiểu never đánh dấu đoạn code không thể chạy tới, và chính điều này giúp kiểm tra đầy đủ các trường hợp.

return fail(...) biên dịch được trong một hàm trả về number vì never gán được cho mọi kiểu: một giá trị kiểu never không bao giờ thực sự xuất hiện.

Hàm không bao giờ trả về

Có hai loại hàm không bao giờ chạy đến cuối: hàm luôn ném lỗi, và hàm có vòng lặp vô hạn. Trình biên dịch kiểm tra lời khẳng định này. Một hàm được chú thích never mà phần cuối vẫn tới được là lỗi TS2534, A function returning 'never' cannot have a reachable end point.

function fail(message: string): never {
  throw new Error(message);
}

function runForever(): never {
  while (true) {
    // poll, serve requests...
  }
}

Việc suy luận kiểu khác nhau tùy cú pháp. Một khai báo hàm chỉ ném lỗi được suy ra là trả về void, còn một arrow function hay function expression chỉ ném lỗi được suy ra là never:

function f1() { throw new Error("x"); }       // () => void
const f2 = () => { throw new Error("x"); };   // () => never

Việc thu hẹp kiểu chỉ coi một lời gọi là ngõ cụt khi tên được gọi có kiểu tường minh trả về never: một khai báo hàm được chú thích : never, như fail ở trên, hoặc một biến có chú thích kiểu, const fail: (m: string) => never = (m) => { throw new Error(m); }. Kiểu never được suy ra thì không tính, và const fail = (m: string): never => ... cũng không, vì ở đó chỉ arrow function được chú thích còn biến thì không.

never và void

voidnever
Hàm kết thúccókhông (ném lỗi hoặc lặp mãi mãi)
Giá trị lúc chạyundefinedkhông có: lời gọi không bao giờ tạo ra giá trị
Code sau lời gọitới đượckhông tới được
Gán được cho kiểu khácchỉ cho void, unknown, anycho mọi kiểu
Cách dùng điển hìnhcallback, trình xử lý sự kiện, hàm có tác dụng phụfail(), assertNever(), vòng lặp vô hạn

Khác biệt thực tế thể hiện ở việc thu hẹp kiểu. Sau if (!user) fail("no user"), trình biên dịch chỉ biết user đã được định nghĩa ở dòng tiếp theo nếu fail trả về never. Với kiểu trả về void, nó giả định chương trình có thể chạy tiếp.

Kiểm tra đầy đủ với never

Mỗi case của một switch trên union thu hẹp giá trị. Khi mọi thành viên đã được xử lý, thứ còn lại trong default là never. Gán nó cho một biến never biến câu "tôi đã xử lý mọi trường hợp" thành điều trình biên dịch kiểm chứng:

Giờ thêm thành viên thứ ba vào union mà không thêm case:

index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.

Lỗi nêu đúng tên thành viên bạn đã quên. Thêm case "triangle": return (shape.base * shape.height) / 2; là code biên dịch lại được. Hàm hỗ trợ assertNever là dạng dùng lại được của cùng phép kiểm tra đó, và câu lệnh throw của nó vẫn có ý nghĩa lúc chạy: dữ liệu từ JSON hoặc từ một client cũ có thể chứa một kind mà kiểu cho là không thể có. Mẫu này là xương sống của discriminated union.

Thu hẹp kiểu về never

Điều tương tự xảy ra với mọi kiểu thu hẹp, không chỉ switch. Khi mọi khả năng đã bị loại, biến có kiểu never:

Nếu sau này bạn mở rộng tham số thành string | number | boolean | bigint, dòng const nothing: never = x sẽ thành lỗi, chỉ ra đúng hàm cần cập nhật.

never biến mất trong union

never là tập giá trị rỗng, nên thêm nó vào một union không thay đổi gì: string | never chỉ là string. Đó là cách conditional type lọc union. Một nhánh trả về never sẽ loại bỏ thành viên đó:

Exclude<T, U> và Extract<T, U> có sẵn hoạt động đúng theo cách này. Trong intersection thì ngược lại: string & never là never.

Kiểu bất khả thi trở thành never

Một intersection mà không giá trị nào thỏa mãn được sẽ rút gọn thành never:

type A = string & number;                   // never
type B = { kind: "a" } & { kind: "b" };     // never

Đọc một thuộc tính của giá trị kiểu B sẽ báo lý do: Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. Khi một kiểu bạn tạo ra hóa ra là never, hãy tìm hai phần mâu thuẫn với nhau.

never, unknown và any

KiểuGiá trị chứa đượcGán được choNhận
unknownmọi giá trịchỉ unknown và anymọi thứ
anymọi giá trịmọi thứ trừ nevermọi thứ
neverkhông giá trị nàomọi thứchỉ never

unknown là đỉnh của hệ thống phân cấp kiểu và never là đáy. any hoàn toàn không thuộc hệ thống phân cấp: nó tắt việc kiểm tra.

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

Kiểu never trong TypeScript là gì?

never là kiểu không có giá trị nào. Không gì gán được cho nó (trừ một never khác), còn nó thì gán được cho mọi kiểu. Nó xuất hiện là kiểu trả về của các hàm luôn ném lỗi hoặc lặp vô hạn, là kiểu của một biến sau khi việc thu hẹp đã loại hết mọi khả năng, và là kết quả của các kiểu bất khả thi như string & number.

never và void khác nhau thế nào?

Một hàm trả về void kết thúc bình thường; nó chỉ không trả về giá trị hữu ích nào (lúc chạy nó trả về undefined). Một hàm trả về never hoàn toàn không kết thúc: nó ném lỗi hoặc chạy mãi mãi. Code sau lời gọi một hàm never là không thể tới được, và TypeScript xử lý nó như vậy khi thu hẹp kiểu.

Làm sao để kiểm tra đầy đủ các trường hợp trong TypeScript?

Trong nhánh default của một switch trên union, hãy gán giá trị cho một biến kiểu never, hoặc truyền nó vào một hàm assertNever(value: never): never có ném lỗi. Nếu mọi trường hợp đều được xử lý, giá trị ở đó là never và code biên dịch được. Nếu thiếu một trường hợp, trình biên dịch báo rằng thành viên bị thiếu không gán được cho never.

Vì sao kiểu của tôi lại là never?

Thường là vì TypeScript đã thu hẹp loại bỏ mọi lựa chọn (ví dụ sau khi kiểm tra typeof x === "string" và typeof x === "number" trên một string | number), hoặc vì một intersection bất khả thi: string & number, hoặc hai kiểu đối tượng có thuộc tính chung mang literal type mâu thuẫn. Rê chuột lên kiểu trong trình soạn thảo để xem bước nào tạo ra nó.

"is not assignable to type never" nghĩa là gì?

Code đã cố đặt một giá trị thật vào chỗ chỉ cho phép never. Trong phép kiểm tra đầy đủ, điều đó nghĩa là một thành viên của union chưa được xử lý. Ở chỗ khác, nó thường có nghĩa một mảng bị suy ra là never[] hoặc một intersection đã sụp thành never; hãy thêm chú thích kiểu hoặc sửa các kiểu mâu thuẫn.

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