Nhảy tới nội dung

Kiểu never

Kiểu never là kiểu đặc biệt của TypeScript có nghĩa là "không có giá trị".

Đặc tính của never

Không thể gán gì vào

Không thể gán bất cứ thứ gì vào kiểu never.

ts
const foo: never = 1;
Type '1' is not assignable to type 'never'.2322Type '1' is not assignable to type 'never'.
ts
const foo: never = 1;
Type '1' is not assignable to type 'never'.2322Type '1' is not assignable to type 'never'.

Ngay cả kiểu any cũng không thể gán vào.

ts
const any: any = 1;
const foo: never = any;
Type 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.
ts
const any: any = 1;
const foo: never = any;
Type 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.

Chỉ có kiểu never mới có thể gán vào.

ts
const foo: never = 1 as never;
ts
const foo: never = 1 as never;

Có thể gán vào bất cứ kiểu nào

Kiểu never có thể được gán vào bất kỳ kiểu nào.

ts
const nev = 1 as never;
const a: string = nev; // Gán OK
const b: string[] = nev; // Gán OK
ts
const nev = 1 as never;
const a: string = nev; // Gán OK
const b: string[] = nev; // Gán OK

Không có giá trị nghĩa là gì

"Không có giá trị" của kiểu never nghĩa là gì? Ví dụ, giá trị trả về của hàm luôn gây ra exception. Giá trị trả về không bao giờ có thể lấy được. Do đó, kiểu của giá trị trả về là kiểu never.

ts
function throwError(): never {
throw new Error();
}
ts
function throwError(): never {
throw new Error();
}

Hàm không kết thúc cũng có giá trị trả về là kiểu never.

ts
function forever(): never {
while (true) {} // Vòng lặp vô hạn
}
ts
function forever(): never {
while (true) {} // Vòng lặp vô hạn
}

Giá trị không thể tạo ra cũng trở thành kiểu never. Ví dụ, không thể tạo ra giá trị có thể gán cho cả kiểu number và kiểu string. Do đó, intersection type của kiểu number và kiểu string là kiểu never.

ts
type NumberString = number & string;
type NumberString = never
ts
type NumberString = number & string;
type NumberString = never

Sự khác biệt giữa kiểu void và kiểu never

Kiểu void có thể gán undefined, nhưng never không thể có giá trị.

ts
const ok: void = undefined;
const ng: never = undefined;
Type 'undefined' is not assignable to type 'never'.2322Type 'undefined' is not assignable to type 'never'.
ts
const ok: void = undefined;
const ng: never = undefined;
Type 'undefined' is not assignable to type 'never'.2322Type 'undefined' is not assignable to type 'never'.

Về mặt ý nghĩa, voidnever ở giá trị trả về đều giống nhau là không có giá trị trả về. Điểm khác biệt là hàm có kết thúc hay không. void có nghĩa là hàm được thực thi đến cuối. never có nghĩa là xử lý của hàm bị gián đoạn hoặc thực thi mãi mãi.

KiểuGiá trị trả vềCó kết thúc không
voidKhôngreturn hoặc thực thi đến cuối
neverKhôngBị gián đoạn hoặc thực thi mãi mãi

Do đó, nếu implementation của hàm có giá trị trả về là never có thể chạy đến cuối, TypeScript sẽ báo lỗi compile.

ts
function func(): never {}
A function returning 'never' cannot have a reachable end point.2534A function returning 'never' cannot have a reachable end point.
ts
function func(): never {}
A function returning 'never' cannot have a reachable end point.2534A function returning 'never' cannot have a reachable end point.

Kiểm tra tính đầy đủ (exhaustiveness check) bằng never

Đặc tính không thể gán gì vào của never có thể được ứng dụng cho exhaustiveness check. Exhaustiveness check là việc để compiler kiểm tra xem logic có xử lý hết tất cả các pattern hay không khi xử lý phân nhánh union type.

Ví dụ, có một union type với 3 pattern.

ts
type Extension = "js" | "ts" | "json";
ts
type Extension = "js" | "ts" | "json";

Đây là xử lý phân nhánh chỉ xử lý 2 pattern. Không có tính đầy đủ, nhưng TypeScript không cảnh báo.

Phân nhánh không đầy đủ
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
// Không có phân nhánh cho "json"
}
}
Phân nhánh không đầy đủ
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
// Không có phân nhánh cho "json"
}
}

Cơ bản về exhaustiveness check

Để thực hiện exhaustiveness check, gán giá trị cần kiểm tra tính đầy đủ cho kiểu never trong phân nhánh default. Khi đó, TypeScript sẽ cảnh báo lỗi gán.

Phân nhánh có exhaustiveness check
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
const exhaustivenessCheck: never = ext;
Type '"json"' is not assignable to type 'never'.2322Type '"json"' is not assignable to type 'never'.
break;
}
}
Phân nhánh có exhaustiveness check
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
const exhaustivenessCheck: never = ext;
Type '"json"' is not assignable to type 'never'.2322Type '"json"' is not assignable to type 'never'.
break;
}
}

Exhaustiveness check bằng exception

Khuyến nghị nên định nghĩa class exception cho exhaustiveness check. Class này được thiết kế để nhận kiểu never làm tham số constructor.

Hàm exhaustiveness check
ts
class ExhaustiveError extends Error {
constructor(value: never, message = `Unsupported type: ${value}`) {
super(message);
}
}
Hàm exhaustiveness check
ts
class ExhaustiveError extends Error {
constructor(value: never, message = `Unsupported type: ${value}`) {
super(message);
}
}

Ném exception này trong phân nhánh default. Truyền tham số cần kiểm tra tính đầy đủ vào constructor. Khi làm như vậy, nếu tính đầy đủ không được đáp ứng, TypeScript sẽ cảnh báo lỗi gán.

ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
throw new ExhaustiveError(ext);
Argument of type '"json"' is not assignable to parameter of type 'never'.2345Argument of type '"json"' is not assignable to parameter of type 'never'.
}
}
ts
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
throw new ExhaustiveError(ext);
Argument of type '"json"' is not assignable to parameter of type 'never'.2345Argument of type '"json"' is not assignable to parameter of type 'never'.
}
}

Có 2 lợi ích khi dùng exception.

  1. Có thể xử lý noUnusedLocals
  2. Code có ý thức về runtime

Có thể xử lý noUnusedLocals

Compiler option noUnusedLocals cài đặt có cảnh báo về biến không được sử dụng hay không. Khi option này là true, exhaustiveness check chỉ gán vào biến sẽ gây lỗi compile.

Dù xử lý đủ hết nhưng vẫn bị cảnh báo biến không sử dụng
ts
function func(value: "yes" | "no"): void {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
const exhaustivenessCheck: never = value;
'exhaustivenessCheck' is declared but its value is never read.6133'exhaustivenessCheck' is declared but its value is never read.
break;
}
}
Dù xử lý đủ hết nhưng vẫn bị cảnh báo biến không sử dụng
ts
function func(value: "yes" | "no"): void {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
const exhaustivenessCheck: never = value;
'exhaustivenessCheck' is declared but its value is never read.6133'exhaustivenessCheck' is declared but its value is never read.
break;
}
}

Nếu làm exhaustiveness check bằng exception, lỗi compile về biến không sử dụng sẽ không xảy ra.

Code có ý thức về runtime

Exception là implementation có ý thức hơn về JavaScript sau khi compile. Khi compile code exhaustiveness check bằng gán biến, sẽ sinh ra JavaScript sau.

JavaScript sau khi compile (exhaustiveness check bằng gán biến)
ts
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
const exhaustivenessCheck = value;
break;
}
}
 
JavaScript sau khi compile (exhaustiveness check bằng gán biến)
ts
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
const exhaustivenessCheck = value;
break;
}
}
 

Người không biết TypeScript gốc khi nhìn code này, việc gán vào exhaustivenessCheck có ý đồ không rõ ràng. Hơn nữa, exhaustiveness check không được thực hiện tại runtime.

Exhaustiveness check bằng exception có ý đồ rõ ràng ngay cả khi chỉ nhìn code sau compile. Hơn nữa, check cũng được thực hiện tại runtime. Đây là implementation tốt hơn.

JavaScript sau khi compile (exhaustiveness check bằng exception)
ts
class ExhaustiveError extends Error {
constructor(value, message = `Unsupported type: ${value}`) {
super(message);
}
}
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
throw new ExhaustiveError(value);
}
}
 
JavaScript sau khi compile (exhaustiveness check bằng exception)
ts
class ExhaustiveError extends Error {
constructor(value, message = `Unsupported type: ${value}`) {
super(message);
}
}
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
throw new ExhaustiveError(value);
}
}
 
Chia sẻ kiến thức

never trong TypeScript là kiểu "không có giá trị".

1️⃣Đặc tính 1: Không thể gán gì vào never
2️⃣Đặc tính 2: never có thể gán vào bất cứ thứ gì
💥Có thể dùng cho giá trị trả về của hàm luôn gây exception
👐Khác với void
✅Có thể ứng dụng cho exhaustiveness check

Từ 『Survival TypeScript』

Đăng nội dung này lên X