TypeScript 里奇怪的 void 类型

448 阅读4分钟

前言

在 TypeScript(TS)中,有一种名为void的类型。它代表“空”,但需要注意的是,它与 JavaScript(JS)中的“空”并不相同。

通常,void用于声明函数的返回类型。尽管可以将变量声明为void类型,但我们一般不会这样做,因为这没有实际意义。下面通过示例来探讨原因。

void 类型

  • 声明 void 类型的变量
let name: void; // 声明一个类型为 void 的变量 name
name = 'nanjiu'; // 错误:不能将'string'类型赋值给'void'类型
name = 18; // 错误:不能将'number'类型赋值给'void'类型
name = null; // 错误:不能将'null'类型赋值给'void'类型
name = undefined; // 有效

这意味着当一个变量为void类型时,它只能被赋值为undefined,其他任何值都不被允许。

现在,你可能明白为什么我们通常不声明void类型的变量了。因为它唯一可接受的值是undefined,在实际开发中没有什么用处。

  • 声明函数返回值为 void
    • 显式返回:当函数的返回类型声明为void时,可以在函数内部返回undefined
function sayHello(): void {
  console.log('hello');
  return undefined;
}

const str = sayHello();
console.log(str); // undefined

除了undefined,不能返回其他任何值。 - 隐式返回:在 JS 中,如果函数没有显式返回值,其隐式返回值为undefined。这使得以下代码是有效的:

function sayHello(): void {
  console.log('hello');
}

const str = sayHello();
console.log(str); // undefined

不应依赖 void 值

void的另一个关键特性是,调用者不应依赖其返回值进行任何操作。例如:

let name: void; // 声明一个类型为 void 的变量 name

// 返回类型为 void 的函数
function sayHello(): void {
  console.log('hello');
}

// 函数的返回值为 void,即 undefined
const str = sayHello();
console.log(str); // undefined

// 错误:不能测试"void"表达式的真假性
if (str) {
  console.log('str 存在');
} else {
  console.log('str 不存在');
}

因为在 TS 中void表示“空”,所以不应该用它进行任何操作。

总结:

  • void通常用于声明函数的返回类型,表示“空”。void类型唯一可接受的值是undefined
  • 不应依赖void类型的返回值进行任何操作。

这很简单,主要就是这两个关键点,但下面的示例可能会让你有点惊讶……

type

type是 TS 中的一个关键字,用于创建自定义类型。它允许我们为任何类型创建别名,使类型的复用和扩展更加方便。

例如:

// 创建一个可以是字符串或数字的自定义类型
type strOrnum = string | number;

// 声明一个 strOrnum 类型的变量
let str: strOrnum;
str = 'nanjiu'; // 可以赋值为字符串
str = 18; // 也可以赋值为数字

当然,type有很多强大的功能,如联合类型、交叉类型等,但这里我们不深入探讨。让我们来看一个有趣的问题:

声明函数类型

// 创建一个函数类型,参数为字符串,返回值为 void
type say = (name: string) => void;

// 定义一个 say 类型的函数
let sayHello: say = (name: string) => {
  console.log(`hello ${name}`);
};

sayHello('nanjiu');

这里,我们使用type创建了一个函数类型。该函数接受一个字符串类型的参数,并返回一个void类型的值。

函数返回非 undefined 值

从上面的解释可知,返回类型为void的函数只能返回undefined(显式或隐式)。然而,在这种情况下,可以返回任何值……

// 创建一个函数类型,参数为字符串,返回值为 void
type say = (name: string) => void;

// 定义一个 say 类型的函数
let sayHello: say = (name: string) => {
  console.log(`hello ${name}`);
  return null;
};

const res = sayHello('nanjiu');
console.log(res); // null

为什么会这样呢?乍一看,这个示例似乎与void的定义相矛盾。这是 TS 中的一个 bug 吗?

实际上不是。官方解释是:

这种行为的存在是为了确保以下代码有效。我们知道Array.prototype.push返回一个数字,而Array.prototype.forEach期望其回调函数的返回类型为void

const arr = [1, 2, 3, 4, 5];
const list = [0];

arr.forEach(item => list.push(item));
console.log(list);

这实际上与使用type进行自定义类型声明是一样的:

type callbackfn = (value: number, index: number, array: number[]) => void;

这个函数定义有三个参数:前两个是number类型,第三个是数字数组。函数的返回类型是void

由于我们的回调函数使用了箭头函数简写,它隐式返回list.push(item)。然而,push方法确实有返回值。

item => list.push(item)

等价于:

item => {
  return list.push(item);
}

等价于:

item => {
  return 2;

  // return 3;
  // return 4;
  //...
}

这意味着函数的返回类型变为number,与void返回类型定义不匹配。

因此,为了允许这种简写语法,TypeScript 采用了这种行为。当函数的返回类型被限制为void时,TS 不会严格强制函数不返回任何值。

否则,我们将不得不这样写:

arr.forEach(item => {
  list.push(item);
});
// 或者
arr.forEach(function (item) {
  list.push(item);
});

然而,尽管 TS 不严格强制void返回类型必须返回“空”,但我们仍然不能依赖其返回值进行任何操作。

// 创建一个函数类型,参数为字符串,返回值为 void
type say = (name: string) => void;

// 定义一个 say 类型的函数
let sayHello: say = (name: string) => {
  console.log(`hello ${name}`);
  return name;
};

const res = sayHello('nanjiu');
console.log(res); // 'nanjiu'

if (res) {
  console.log('res');
}