TypeScript void 与 never

在前面的学习中,我们定义变量时经常用到 number、string 等基础类型。但如果我们要给一个 “函数” 定义返回值的类型,特别是当这个函数什么都不返回,或者永远不可能执行完时,此时应该怎么做呢?

这个时候,就需要用到 TypeScript 提供的两种特殊类型:void 和 never 了。

TypeScript 的 “空值” —— void

在 JavaScript 中,如果一个函数没有写 return 语句,它默认会返回 undefined

但在 TypeScript 中,为了代码的严谨性,如果一个函数没有任何返回值,我们通常会将其返回值类型标记为 void。

语法:

function 函数名(): void {
    // 函数体
}

说明:

void 英文意思是 “空”,表示没有任何类型。

示例 1:使用 void 定义函数返回值

// 该函数只负责打印,不需要返回任何数据
function sayHello(name: string): void {
    console.log("Hello,", name);
}

sayHello("Jack");

运行结果如下。

Hello, Jack

分析:

在这个例子中,sayHello() 函数只是在控制台打印了一句话,并没有 return 任何具体的数据。因此,我们把它的返回值类型标记为 void。

如果我们非要在里面写一个 return 666;,此时TypeScript 立马就会飘红报错,这就保证了函数的行为与预期完全一致。

注意: 在 TypeScript 中,我们也可以声明一个变量为 void 类型,但这样做是毫无意义的,因为此时只能把 undefined 赋值给它。因此在绝大多数情况下,void 都是用来限制函数的返回值。

很多小伙伴可能会想:“既然函数不写 return,底层也会返回undefined。那我直接把返回类型写成 undefined,这样不也可以吗?”

答案是:强烈不建议这么做!

在 TypeScript 中,如果把返回类型写成 undefined,那么 TypeScript 会非常较真地要求你的函数体内必须显式地写上 return; 或者 return undefined;。如果你什么都不写,它反而会报错。

而使用 void 的含义是:“作为开发者,我根本不关心这个函数到底返回了什么,调用这个函数的人也不应该去使用它的返回值。” 这是一种更符合逻辑、且不会给编写者带来额外负担的最佳实践。

TypeScript 中 “到不了终点的值” —— never

如果说 void 表示函数 “执行完了、但没留下任何东西”,那么 never 表示的是函数 “永远都不可能执行完、或者执行时必定会抛出异常”。

在 TypeScript 中,never 类型表示的是那些 “永不存在的值” 的类型。在实际开发中,never 最常用于以下 2 种场景:

  • 必然抛出异常的函数。
  • 存在死循环的函数。

示例 2:抛出异常的函数

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

分析:

上面这个函数一旦执行就会抛出错误,程序直接崩溃,永远不可能正常 “返回”。

示例 3:死循环的函数

function infiniteLoop(): never {
    while (true) {
        console.log("根本停不下来!");
    }
}

分析:

这个函数是一个死循环,永远不会有执行完毕的那一刻。对于这种一旦进入就 “有去无回” 的函数,我们就需要使用 never 来标记它的返回值类型。因为它们连返回 undefined 的机会都没有。

TypeScript void 与 never 的区别

很多小伙伴肯定会感到疑惑:“void 和 never 不都是表示函数没返回值吗?它们之间到底有什么区别呢?” 其实它们区别还是比较大的:

  • void:函数能正常执行完毕,只是它刚好不返回具体数据(底层其实返回了 undefined)。
  • never:函数根本无法正常执行完毕(要么报错崩溃、要么死循环跳不出来)。

我们可以这样去理解:

  • void 就像是去买彩票,虽然没中奖(没返回值),但你至少能够平平安安从彩票店走出来。
  • never 就像是进了黑洞,一旦进去,就永远不可能再走出来了。

void vs never

TypeScript never 的高级用法

在真实的项目开发中,never 有一个非常高端实用的玩法:类型穷尽检查。它能利用 TypeScript 的类型收窄特性,帮我们写出绝对安全、不怕需求变更的代码。

示例 4:使用 never 进行穷尽检查

// 定义一个联合类型,表示支持的请求方式
type Method = "GET" | "POST" | "PUT";

function request(method: Method) {
    if (method === "GET") {
        console.log("执行 GET 请求");
    } else if (method === "POST") {
        console.log("执行 POST 请求");
    } else if (method === "PUT") {
        console.log("执行 PUT 请求");
    } else {
        // 如果我们漏掉了某个 method,TypeScript 会在这里报错
        const exhaustiveCheck: never = method;
        console.log(exhaustiveCheck);
    }
}

分析:

在这个例子中,我们使用 if...else 语句处理了所有可能的请求 Method。

假设有一天,团队有小伙伴在 Method 类型里新增了一个 "DELETE"(即 type Method = "GET" | "POST" | "PUT" | "DELETE";),但他忘记在 request() 函数里加上对应的 if 分支。

这个时候,漏掉的 "DELETE" 就会掉进最后的 else 分支里。由于 exhaustiveCheck 的类型是 never(绝对不接受任何类型的值),那么 TypeScript 会立刻抛出错误:

不能将类型 "DELETE" 分配给类型 "never"

这种高级用法可以强迫我们在修改类型后,把所有相关的业务逻辑全部补全,大大降低了项目上线后出 bug 的风险。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号