基础类型与注解

6 阅读1分钟

基础类型与注解

深入学习 TypeScript 注解、基础类型、对象字面量、数组、函数类型、异步函数,并通过实践练习进行动手学习。

4

既然我们已经介绍了 TypeScript 的“为什么”,现在是时候开始学习“怎么做”了。我们将涵盖类型注解和类型推断等关键概念,以及如何开始编写类型安全的函数。

打下坚实的基础非常重要,因为你稍后学习的所有内容都将建立在本章所学知识之上。

基础注解

作为 TypeScript 开发者,你最常做的事情之一就是给代码添加注解。注解告诉 TypeScript 某个东西应该是什么类型。

注解通常会使用 : ——这用于告诉 TypeScript 一个变量或函数参数是某种特定的类型。

函数参数注解

你将会用到的最重要的注解之一是函数参数注解。

例如,这里有一个 logAlbumInfo 函数,它接收一个 title 字符串、一个 trackCount 数字和一个 isReleased布尔值:

const logAlbumInfo = (
  title: string,
  trackCount: number,
  isReleased: boolean,
) => {
  // 实现代码
};

每个参数的类型注解使得 TypeScript 能够检查传递给函数的参数是否为正确的类型。如果类型不匹配,TypeScript 会在有问题的参数下方显示一条红色的波浪线。

logAlbumInfo("Black Gold", false, 15);
类型 'boolean' 的参数不能赋给类型 'number' 的参数。2345

在上面的例子中,我们首先会在 false 下方得到一个错误,因为布尔值不能赋给数字。

logAlbumInfo("Black Gold", 20, 15);
类型 'number' 的参数不能赋给类型 'boolean' 的参数。2345

修复那个错误后,我们会在 15 下方得到一个错误,因为数字不能赋给布尔值。

变量注解

除了函数参数,你也可以注解变量。

这里是一些变量及其相关类型的示例。

let albumTitle: string = "Midnights";
let isReleased: boolean = true;
let trackCount: number = 13;

注意每个变量名后面是如何跟着 : 及其原始类型,然后才设置其值的。

变量注解用于明确告诉 TypeScript 我们期望变量的类型是什么。

一旦变量用特定的类型注解声明后,TypeScript 将确保该变量始终与你指定的类型兼容。

例如,这样的重新赋值是可行的:

let albumTitle: string = "Midnights";
albumTitle = "1989";

但这个会显示错误:

let isReleased: boolean = true;
isReleased = "yes";
类型 'string' 不能赋值给类型 'boolean'。2322

TypeScript 的静态类型检查能够在编译时发现错误,这在你编写代码时就在后台进行。

在上面 isReleased 的例子中,错误信息是:

类型 'string' 不能赋值给类型 'boolean'

换句话说,TypeScript 告诉我们它期望 isReleased 是一个布尔值,但却收到了一个 string

在运行代码之前就被警告这类错误真是太好了!

基础类型

TypeScript 有许多基础类型可以用来注解你的代码。以下是一些最常见的:

let example1: string = "Hello World!";
let example2: number = 42;
let example3: boolean = true;
let example4: symbol = Symbol();
let example5: bigint = 123n;
let example6: null = null;
let example7: undefined = undefined;

这些类型中的每一个都用于告诉 TypeScript 变量或函数参数应该是什么类型。

你可以在 TypeScript 中表达更复杂的类型:数组、对象、函数等等。我们将在后续章节中介绍这些。

类型推断

TypeScript 赋予你注解代码中几乎任何值、变量或函数的能力。你可能会想:“等等,我需要注解所有东西吗?那会增加很多额外的代码。”

事实证明,TypeScript 可以从你的代码运行的上下文中推断出很多信息。

变量并非总是需要注解

让我们再看一下我们的变量注解示例,但去掉注解:

let albumTitle = "Midnights";
let isReleased = true;
let trackCount = 13;

我们没有添加注解,但 TypeScript 并没有报错。这是怎么回事?

尝试将你的光标悬停在每个变量上。

// 悬停在每个变量名上
let albumTitle: string;
let isReleased: boolean;
let trackCount: number;

即使它们没有被注解,TypeScript 仍然能识别出它们各自应该是什么类型。这就是 TypeScript 从使用中推断变量类型。

它的行为就像我们已经注解了它一样,如果我们尝试给它赋一个与最初赋值不同的类型,它会警告我们:

let isReleased = true;
isReleased = "yes";
类型 'string' 不能赋值给类型 'boolean'。2322

并且还会为我们提供变量的自动补全功能:

albumTitle.toUpper; // 在自动补全中显示 `toUpperCase`

这是 TypeScript 一个极其强大的部分。这意味着你基本上可以注解变量,而你的 IDE 仍然知道事物的类型。

函数参数总是需要注解

但是类型推断并非在所有地方都有效。让我们看看如果从 logAlbumInfo 函数的参数中移除类型注解会发生什么:

const logAlbumInfo = (
  title,  // 参数 'title' 隐式具有 'any' 类型。7006
  trackCount, // 参数 'trackCount' 隐式具有 'any' 类型。7006
  isReleased, // 参数 'isReleased' 隐式具有 'any' 类型。7006
) => {
  // 函数体的其余部分
};

TypeScript 本身无法推断参数的类型,所以它在每个参数名下显示错误。

这是因为函数与变量非常不同。TypeScript 可以看到哪个值赋给了哪个变量,所以它可以对类型做出很好的猜测。

但是 TypeScript 不能仅从函数参数就判断出它应该是什么类型。当你不注解它时,它默认类型为 any ——一个可怕的、不安全的类型。

它也无法从使用中检测。如果我们有一个接收两个参数的 'add' 函数,TypeScript 将无法判断它们应该是数字:

function add(a, b) { // 参数 'b' 隐式具有 'any' 类型。7006 参数 'a' 隐式具有 'any' 类型。7006
  return a + b;
}

ab 可能是字符串、布尔值或其他任何东西。TypeScript 无法从函数体中知道它们应该是什么类型。

所以,当你声明一个命名函数时,它们的参数在 TypeScript 中总是需要注解。

any 类型

我们在“函数参数总是需要注解”一节中遇到的错误相当吓人:

参数 'title' 隐式具有 'any' 类型。

当 TypeScript 不知道某个东西是什么类型时,它会给它分配 any 类型。

这个类型破坏了 TypeScript 的类型系统。它关闭了对其赋值对象的类型安全检查。

这意味着任何东西都可以赋给它,它的任何属性都可以被访问/赋值,并且它可以像函数一样被调用。

let anyVariable: any = "This can be anything!";
anyVariable(); // 没有错误
anyVariable.deep.property.access; // 没有错误

上面的代码在运行时会出错,但 TypeScript 并没有给我们警告!

所以,使用 any 可以用来关闭 TypeScript 中的错误。当一个类型太复杂难以描述时,它可以作为一个有用的“逃生舱口”。

但是过度使用 any 会违背使用 TypeScript 的初衷,所以最好尽可能避免使用它——无论是隐式的还是显式的。

练习

练习 1:带函数参数的基础类型

让我们从一个 add 函数开始,它接收两个布尔参数 ab,并返回 a + b

export const add = (a: boolean, b: boolean) => {
  return a + b; // 运算符 '+' 不能应用于类型 'boolean''boolean'2365
};

通过调用 add 函数创建了一个 result 变量。然后检查 result 变量是否等于一个 number

const result = add(1, 2); // 类型 'number' 的参数不能赋给类型 'boolean' 的参数。2345
type test = Expect<Equal<typeof result, number>>; // 类型 'false' 不满足约束 'true'2344

目前,代码中有一些错误,用红色波浪线标记了出来。

第一个是在 add 函数的 return 行,我们有 a + b

运算符 '+' 不能应用于类型 'boolean''boolean'

add 函数调用的 1 参数下方也有一个错误:

类型 'number' 的参数不能赋给类型 'boolean' 的参数

最后,我们可以看到我们的 test 结果有一个错误,因为 result 当前被类型化为 any,这不等于 number

你的挑战是考虑我们如何更改类型以消除错误,并确保 result 是一个 number。你可以将鼠标悬停在 result 上进行检查。

练习 1:带函数参数的基础类型

练习 2:注解空参数

这里我们有一个 concatTwoStrings 函数,其结构与 add 函数相似。它接收两个参数 ab,并返回一个字符串。

const concatTwoStrings = (a, b) => { // 参数 'b' 隐式具有 'any' 类型。7006 参数 'a' 隐式具有 'any' 类型。7006
  return [a, b].join(" ");
};

目前 ab 参数上有错误,它们没有用类型注解。

"Hello""World" 调用 concatTwoStringsresult 并检查它是否为 string 时没有显示任何错误:

const result = concatTwoStrings("Hello", "World");
type test = Expect<Equal<typeof result, string>>;

你的任务是为 concatTwoStrings 函数添加一些函数参数注解以消除错误。

练习 2:注解空参数

练习 3:基础类型

正如我们所见,当类型不匹配时,TypeScript 会显示错误。

这组示例向我们展示了 TypeScript 提供给我们用来描述 JavaScript 的基础类型:

export let example1: string = "Hello World!";
export let example2: string = 42; // 类型 'number' 不能赋值给类型 'string'。2322
export let example3: string = true; // 类型 'boolean' 不能赋值给类型 'string'。2322
export let example4: string = Symbol(); // 类型 'symbol' 不能赋值给类型 'string'。2322
export let example5: string = 123n; // 类型 'bigint' 不能赋值给类型 'string'。2322

注意,冒号 : 用于注解每个变量的类型,就像它用于类型化函数参数一样。

你还会注意到有几个错误。

将鼠标悬停在每个带下划线的变量上将显示任何相关的错误消息。

例如,悬停在 example2 上将显示:

类型 'number' 不能赋值给类型 'string'

example3 的类型错误告诉我们:

类型 'boolean' 不能赋值给类型 'string'

更改每个变量上注解的类型以消除错误。

练习 3:基础类型

练习 4:any 类型

这里有一个名为 handleFormData 的函数,它接受一个类型为 anye。该函数阻止默认的表单提交行为,然后从表单数据创建一个对象并返回它:

const handleFormData = (e: any) => {
  e.preventDefault();
  const data = new FormData(e.terget); // 注意:这里有一个拼写错误 terget 而不是 target
  const value = Object.fromEntries(data.entries());
  return value;
};

这里有一个针对该函数的测试,它创建一个表单,设置 innerHTML 来添加一个输入框,然后手动提交表单。当它提交时,我们期望该值等于我们嵌入表单中的值:

it("应该处理表单提交", () => {
  const form = document.createElement("form");
  form.innerHTML = `
<input name="name" value="John Doe"></input> 
`; // 注意:</input> 不是标准的 HTML,应该是 <input ... /> 或 <input ... >
  form.onsubmit = (e) => {
    const value = handleFormData(e);
    expect(value).toEqual({ name: "John Doe" });
  };
  form.requestSubmit();
  expect.assertions(1);
});

注意:原文中 标签在这里似乎是 Exercise 标题的误植,已修正为 </input>,但更规范的是自闭合标签。

请注意,这不是测试表单的常规方法,但它提供了一种更广泛地测试示例 handleFormData 函数的方法。

在代码的当前状态下,没有出现红色波浪线。

然而,当用 Vitest 运行测试时,我们会得到类似以下的错误:

此错误源于 "any.problem.ts" 测试文件。这并不意味着错误是在文件内部抛出的,而是在它运行时发生的。
可能导致错误的最新测试是 "Should handle a form submit"。这可能意味着以下之一:
- Vitest 运行此测试时抛出了错误。
- 如果错误在测试完成后发生,这是错误抛出前记录的最后一个测试。

为什么会发生这个错误?为什么 TypeScript 没有在这里给我们一个错误?

我给你一个线索。我在里面藏了一个讨厌的拼写错误。你能修复它吗?

练习 4:any 类型

解决方案 1:带函数参数的基础类型

常识告诉我们 add 函数中的 boolean 类型应该被替换为某种 number 类型。

如果你来自另一种语言,你可能会尝试使用 intfloat,但 TypeScript 只有 number 类型:

function add(a: number, b: number) {
  return a + b;
}

进行此更改可以解决错误,并且还为我们带来了一些额外的好处。

如果我们尝试用字符串而不是数字来调用 add 函数,我们会得到一个错误,提示类型 string 不能赋给类型 number

add("something", 2); // 类型 'string' 的参数不能赋给类型 'number' 的参数。2345

不仅如此,我们函数的结果现在也被推断出来了:

const result = add(1, 2);
        // const result: number

所以 TypeScript 不仅可以推断变量,还可以推断函数的返回类型。

解决方案 2:注解空参数

正如我们所知,函数参数在 TypeScript 中总是需要注解。

所以,让我们更新函数声明参数,使 ab 都被指定为 string

const concatTwoStrings = (a: string, b: string) => {
  return [a, b].join(" ");
};

这个改动修复了错误。

额外加分题:返回类型将被推断为什么类型?

const result = concatTwoStrings("Hello", "World");
        // const result: string
解决方案 3:更新基础类型

每个示例都代表了 TypeScript 的基础类型,并应如下注解:

let example1: string = "Hello World!";
let example2: number = 42;
let example3: boolean = true;
let example4: symbol = Symbol();
let example5: bigint = 123n;

我们已经见过 stringnumberbooleansymbol 类型用于 Symbol,它们用于确保属性键的唯一性。bigint 类型用于那些对于 number 类型来说太大的数字。

然而,在实践中,你大多不会像这样注解变量。如果我们移除显式的类型注解,将完全没有错误:

let example1 = "Hello World!";
let example2 = 42;
let example3 = true;
let example4 = Symbol();
let example5 = 123n;

了解这些基础类型非常有用,即使你并不总是需要在变量声明中使用它们。

解决方案 4:any 类型

在这种情况下,使用 any 对我们完全没有帮助。事实上,any 注解似乎实际上关闭了类型检查!

使用 any 类型,我们可以对变量做任何我们想做的事情,TypeScript 不会阻止它。

使用 any 还会禁用像自动补全这样的有用功能,这可以帮助你避免拼写错误。

没错——上面代码中的错误是由创建 FormDatae.terget 而不是 e.target 的拼写错误引起的!

const handleFormData = (e: any) => {
  e.preventDefault();
  const data = new FormData(e.terget); // e.terget! 哎呀!
  const value = Object.fromEntries(data.entries());
  return value;
};

如果 e 被正确地类型化,这个错误会立即被 TypeScript 捕获。我们将来会回到这个例子,看看正确的类型化方式。

当你遇到难以确定如何正确类型化某事物时,使用 any 可能看起来是一个快速的解决方案,但它以后可能会反过来困扰你。

对象字面量类型

现在我们已经对基础类型进行了一些探索,让我们转向对象类型。

对象类型用于描述对象的结构。对象的每个属性都可以有自己的类型注解。

在定义对象类型时,我们使用花括号来包含属性及其类型:

const talkToAnimal = (animal: { name: string; type: string; age: number }) => {
  // 函数体的其余部分
};

这种花括号语法称为对象字面量类型。

可选对象属性

我们可以使用 ? 运算符将 age 属性标记为可选:

const talkToAnimal = (animal: { name: string; type: string; age?: number }) => {
  // 函数体的其余部分
};

对象字面量类型注解的一个很酷的地方是,它们在你输入时为属性名提供自动补全。

例如,当调用 talkToAnimal 时,它会为你提供一个自动补全下拉列表,其中包含 nametypeage 属性的建议。

这个功能可以节省你很多时间,并且在你有多个名称相似的属性时,也有助于避免拼写错误。

练习

练习 1:对象字面量类型

这里我们有一个 concatName 函数,它接受一个带有 firstlast 键的 user 对象:

const concatName = (user) => { // 参数 'user' 隐式具有 'any' 类型。7006
  return `${user.first} ${user.last}`;
};

测试期望返回全名,并且测试通过:

it("应该返回全名", () => {
  const result = concatName({
    first: "John",
    last: "Doe",
  });
  type test = Expect<Equal<typeof result, string>>;
  expect(result).toEqual("John Doe");
});

然而,concatName 函数中的 user 参数上有一个熟悉的错误:

参数 'user' 隐式具有 'any' 类型。

concatName 函数体我们可以看出,它期望 user.firstuser.last 是字符串。

我们如何类型化 user 参数以确保它具有这些属性并且它们是正确的类型?

练习 1:对象字面量类型

练习 2:可选属性类型

这是 concatName 函数的一个更新版本,如果未提供姓氏,则仅返回名字:

const concatName = (user: { first: string; last: string }) => {
  if (!user.last) {
    return user.first;
  }
  return `${user.first} ${user.last}`;
};

和以前一样,当我们测试函数在未提供姓氏时仅返回名字的功能时,TypeScript 给我们一个错误:

const result = concatName({ // 类型 '{ first: string; }' 的参数不能赋给类型 '{ first: string; last: string; }' 的参数。
                          // 类型 '{ first: string; }' 中缺少属性 'last',但类型 '{ first: string; last: string; }' 中需要该属性。2345
  first: "John",
});

错误告诉我们缺少一个属性,但这个错误是不正确的。我们确实希望支持只包含 first 属性的对象。换句话说,last 需要是可选的。

你将如何更新此函数以修复错误?

练习 2:可选属性类型

解决方案 1:对象字面量类型

为了将 user 参数注解为一个对象,我们可以使用花括号语法 {}

让我们首先将 user 参数注解为一个空对象:

const concatName = (user: {}) => {
  return `${user.first} ${user.last}`; // 类型 '{}' 上不存在属性 'last'。2339 类型 '{}' 上不存在属性 'first'。2339
};

错误改变了。这算是一种进步。错误现在出现在函数返回语句中的 .first.last 下方。

为了修复这些错误,我们需要将 firstlast 属性添加到类型注解中。

const concatName = (user: { first: string; last: string }) => {
  return `${user.first} ${user.last}`;
};

现在 TypeScript 知道 userfirstlast 属性都是字符串,并且测试通过了。

解决方案 2:可选属性类型

与我们将函数参数设置为可选类似,我们可以使用 ? 来指定对象的属性是可选的。

正如在之前的练习中所见,我们可以向函数参数添加问号,使其成为可选的:

function concatName(user: { first: string; last?: string }) {
  // 实现
}

添加 ?: 向 TypeScript 表明该属性不必存在。

如果我们将鼠标悬停在函数体内的 last 属性上,我们会看到 last 属性是 string | undefined

// 悬停在 `user.last` 上
(property) last?: string | undefined

这意味着它是 stringundefined。这是 TypeScript 的一个有用特性,我们将来会更多地看到它。

类型别名

到目前为止,我们一直在内联声明我们所有的类型。对于这些简单的例子来说这没问题,但在实际应用中,我们会有很多在整个应用中重复的类型。

这些可能是用户、产品或其他特定领域的类型。我们不希望在每个需要它的文件中都重复相同的类型定义。

这就是 type 关键字发挥作用的地方。它允许我们一次性定义一个类型,并在多个地方使用它。

type Animal = {
  name: string;
  type: string;
  age?: number;
};

这就是所谓的类型别名。这是一种给类型命名的方法,然后在任何需要使用该类型的地方使用该名称。

要创建一个带有 Animal 类型的新变量,我们会在变量名后添加它作为类型注解:

let pet: Animal = {
  name: "Karma",
  type: "cat",
};

我们也可以在函数中使用 Animal 类型别名来代替对象类型注解:

const getAnimalDescription = (animal: Animal) => {
  // 实现
};

并用我们的 pet 变量调用该函数:

const desc = getAnimalDescription(pet);

类型别名可以是对象,但它们也可以使用基础类型:

type Id = string | number;

我们稍后会看这种语法,但它基本上是说一个 Id 可以是 stringnumber

使用类型别名是确保类型定义有单一真实来源的好方法,这使得将来进行更改更加容易。

###跨模块共享类型

类型别名可以在它们自己的 .ts 文件中创建,并导入到你需要它们的文件中。这在多处共享类型或类型定义变得过大时非常有用:

// 在 shared-types.ts 中
export type Animal = {
  width: number;
  height: number;
};

// 在 index.ts 中
import { Animal } from "./shared-types";

按照惯例,你甚至可以创建自己的 .types.ts 文件。这有助于将类型定义与你的其他代码分开。

练习

练习 1:type 关键字

这里有一些在多个地方使用相同类型的代码:

const getRectangleArea = (rectangle: { width: number; height: number }) => {
  return rectangle.width * rectangle.height;
};

const getRectanglePerimeter = (rectangle: {
  width: number;
  height: number;
}) => {
  return 2 * (rectangle.width + rectangle.height);
};

getRectangleAreagetRectanglePerimeter 函数都接收一个具有 widthheight 属性的 rectangle 对象。

每个函数的测试都按预期通过:

it("应该返回矩形的面积", () => {
  const result = getRectangleArea({
    width: 10,
    height: 20,
  });
  type test = Expect<Equal<typeof result, number>>;
  expect(result).toEqual(200);
});

it("应该返回矩形的周长", () => {
  const result = getRectanglePerimeter({
    width: 10,
    height: 20,
  });
  type test = Expect<Equal<typeof result, number>>;
  expect(result).toEqual(60);
});

尽管一切都按预期工作,但仍有机会进行重构以整理代码。

你如何使用 type 关键字使此代码更具可读性?

练习 1:type 关键字

解决方案 1:type 关键字

你可以使用 type 关键字创建一个具有 widthheight 属性的 Rectangle 类型:

type Rectangle = {
  width: number;
  height: number;
};

创建类型别名后,我们可以更新 getRectangleAreagetRectanglePerimeter 函数以使用 Rectangle 类型:

const getRectangleArea = (rectangle: Rectangle) => {
  return rectangle.width * rectangle.height;
};

const getRectanglePerimeter = (rectangle: Rectangle) => {
  return 2 * (rectangle.width + rectangle.height);
};

这使得代码更加简洁,并为 Rectangle 类型提供了单一的真实来源。

数组和元组

数组

你也可以在 TypeScript 中描述数组的类型。有两种不同的语法可以做到这一点。

第一种选项是方括号语法。这种语法与我们目前为止所做的类型注解类似,但在末尾添加了两个方括号以指示数组。

let albums: string[] = [  "Rubber Soul",  "Revolver",  "Sgt. Pepper's Lonely Hearts Club Band",];
let dates: number[] = [1965, 1966, 1967];

第二种选项是显式使用 Array 类型,并在尖括号中包含数组将持有的数据类型:

let albums: Array<string> = [
  "Rubber Soul",
  "Revolver",
  "Sgt. Pepper's Lonely Hearts Club Band",
];

这两种语法是等效的,但在创建数组时,方括号语法更简洁一些。TypeScript 呈现错误消息的方式也是如此。不过,请记住尖括号语法——我们稍后会看到更多它的例子。

对象数组

在指定数组类型时,你可以使用任何内置类型、内联类型或类型别名:

type Album = {
  artist: string;
  title: string;
  year: number;
};

let selectedDiscography: Album[] = [
  {
    artist: "The Beatles",
    title: "Rubber Soul",
    year: 1965,
  },
  {
    artist: "The Beatles",
    title: "Revolver",
    year: 1966,
  },
];

如果你尝试用一个与类型不匹配的项来更新数组,TypeScript 会给你一个错误:

selectedDiscography.push({ name: "Karma", type: "cat" });
对象字面量只能指定已知的属性,而 'name' 在类型 'Album' 中不存在。2353

元组

元组允许你指定一个具有固定数量元素的数组,其中每个元素都有其自己的类型。

创建元组类似于数组的方括号语法——只是方括号包含类型,而不是紧邻变量名:

// 元组
let album: [string, number] = ["Rubber Soul", 1965];

// 数组
let albums: string[] = [
  "Rubber Soul",
  "Revolver",
  "Sgt. Pepper's Lonely Hearts Club Band",
];

元组对于将相关信息组合在一起而无需创建新类型非常有用。

例如,如果我们想将一张专辑与其播放次数分组,我们可以这样做:

let albumWithPlayCount: [Album, number] = [
  {
    artist: "The Beatles",
    title: "Revolver",
    year: 1965,
  },
  10000,
];
命名元组

为了给元组增加更多清晰度,可以在方括号内为每种类型添加名称:

type MyTuple = [album: Album, playCount: number];

当你的元组有很多元素,或者你想让代码更具可读性时,这会很有帮助。

练习

练习 1:数组类型

考虑以下购物车代码:

type ShoppingCart = {
  userId: string;
};

const processCart = (cart: ShoppingCart) => {
  // 在这里对购物车进行一些操作
};

processCart({
  userId: "user123",
  items: ["item1", "item2", "item3"], // 对象字面量只能指定已知的属性,而 'items' 在类型 'ShoppingCart' 中不存在。2353
});

我们有一个 ShoppingCart 的类型别名,它当前有一个类型为 stringuserId 属性。

processCart 函数接收一个类型为 ShoppingCartcart 参数。它的实现目前不重要。

重要的是当我们调用 processCart 时,我们传入了一个带有 userId 和一个 items 属性的对象,items 属性是一个字符串数组。

items 下方有一个错误,内容如下:

类型 '{ userId: string; items: string[]; }' 的参数不能赋给类型 'ShoppingCart' 的参数。
对象字面量只能指定已知的属性,而 'items' 在类型 'ShoppingCart' 中不存在。

正如错误消息指出的,ShoppingCart 类型上当前没有名为 items 的属性。

你将如何修复此错误?

练习 1:数组类型

练习 2:对象数组

考虑这个 processRecipe 函数,它接收一个 Recipe 类型:

type Recipe = {
  title: string;
  instructions: string;
};

const processRecipe = (recipe: Recipe) => {
  // 在这里对菜谱进行一些操作
};

processRecipe({
  title: "Chocolate Chip Cookies",
  ingredients: [ // 对象字面量只能指定已知的属性,而 'ingredients' 在类型 'Recipe' 中不存在。2353
    { name: "Flour", quantity: "2 cups" },
    { name: "Sugar", quantity: "1 cup" },
  ],
  instructions: "...",
});

该函数被调用时传入一个包含 titleinstructionsingredients 属性的对象,但目前存在错误,因为 Recipe 类型当前没有 ingredients 属性:

类型 '{title: string; ingredients: { name: string; quantity: string; }[]; instructions: string; }' 的参数不能赋给类型 'Recipe' 的参数。
对象字面量只能指定已知的属性,而 'ingredients' 在类型 'Recipe' 中不存在。

结合你所看到的关于对象属性类型化和使用数组的知识,你将如何为 Recipe 类型指定 ingredients

练习 2:对象数组

练习 3:元组

这里我们有一个 setRange 函数,它接收一个数字数组:

const setRange = (range: Array<number>) => {
  const x = range[0];
  const y = range[1];
  // 在这里对 x 和 y 进行一些操作
  // x 和 y 都应该是数字!
  type tests = [
    Expect<Equal<typeof x, number>>, // 类型 'false' 不满足约束 'true'2344
    Expect<Equal<typeof y, number>>, // 类型 'false' 不满足约束 'true'2344
  ];
};

在函数内部,我们获取数组的第一个元素并将其赋给 x,获取数组的第二个元素并将其赋给 y

setRange 函数内部有两个测试目前失败。

使用 // @ts-expect-error 指令,我们发现还有几个错误需要修复。回想一下,此指令告诉 TypeScript 我们知道下一行会有错误,所以忽略它。但是,如果我们说我们期望一个错误但实际上没有错误,我们会在实际的 //@ts-expect-error 行上看到红色波浪线。

// @ts-expect-error 参数太少
// 未使用的 '@ts-expect-error' 指令。2578
setRange([0]);

// @ts-expect-error 参数太多
// 未使用的 '@ts-expect-error' 指令。2578
setRange([0, 10, 20]);

setRange 函数的代码需要更新类型注解,以指定它只接受一个包含两个数字的元组。

练习 3:元组

练习 4:元组的可选成员

这个 goToLocation 函数接收一个坐标数组。每个坐标都有 latitudelongitude,它们都是数字,以及一个可选的 elevation,它也是一个数字:

const goToLocation = (coordinates: Array<number>) => {
  const latitude = coordinates[0];
  const longitude = coordinates[1];
  const elevation = coordinates[2];
  // 在这里对 latitude, longitude, 和 elevation 进行一些操作
  type tests = [
    Expect<Equal<typeof latitude, number>>,
    Expect<Equal<typeof longitude, number>>,
    Expect<Equal<typeof elevation, number | undefined>>, // 类型 'false' 不满足约束 'true'2344
  ];
};

你的挑战是更新 coordinates 参数的类型注解,以指定它应该是一个包含三个数字的元组,其中第三个数字是可选的。

练习 4:元组的可选成员

解决方案 1:数组类型

对于 ShoppingCart 示例,使用方括号语法定义一个 item 字符串数组如下所示:

type ShoppingCart = {
  userId: string;
  items: string[];
};

有了这个,我们必须传入 items 作为一个数组。单个字符串或其他类型将导致类型错误。

另一种语法是显式编写 Array 并在尖括号内传递类型:

type ShoppingCart = {
  userId: string;
  items: Array<string>;
};
解决方案 2:对象数组

有几种不同的方法来表达对象数组。

一种方法是创建一个新的 Ingredient 类型,我们可以用它来表示数组中的对象:

type Ingredient = {
  name: string;
  quantity: string;
};

然后可以更新 Recipe 类型以包含一个类型为 Ingredient[]ingredients 属性:

type Recipe = {
  title: string;
  instructions: string;
  ingredients: Ingredient[];
};

这个解决方案读起来很好,修复了错误,并有助于创建我们领域模型的心理地图。

如前所示,使用 Array<Ingredient> 语法也同样有效:

type Recipe = {
  title: string;
  instructions: string;
  ingredients: Array<Ingredient>;
};

也可以使用方括号在 Recipe 类型上以内联对象字面量的形式指定 ingredients 属性:

type Recipe = {
  title: string;
  instructions: string;
  ingredients: {
    name: string;
    quantity: string;
  }[];
};

或者使用 Array<>

type Recipe = {
  title: string;
  instructions: string;
  ingredients: Array<{
    name: string;
    quantity: string;
  }>;
};

内联方法很有用,但我更喜欢将它们提取到一个新的类型中。这意味着如果你的应用程序的另一部分需要使用 Ingredient 类型,它也可以使用。

解决方案 3:元组

在这种情况下,我们会更新 setRange 函数以使用元组语法而不是数组语法:

const setRange = (range: [number, number]) => {
  // 函数体的其余部分
};

如果你想为元组增加更多清晰度,可以为每种类型添加名称:

const setRange = (range: [x: number, y: number]) => {
  // 函数体的其余部分
};
解决方案 4:元组的可选成员

一个好的开始是将 coordinates 参数更改为 [number, number, number | undefined] 的元组:

const goToLocation = (coordinates: [number, number, number | undefined]) => {};

这里的问题是,虽然元组的第三个成员可以是数字或 undefined,但函数仍然期望传入某些东西。手动传入 undefined 并不是一个好的解决方案。

结合使用命名元组和可选操作符 ? 是一个更好的解决方案:

const goToLocation = (
  coordinates: [latitude: number, longitude: number, elevation?: number],
) => {};

值是清晰的,并且使用 ? 运算符指定 elevation 是一个可选的数字。它几乎看起来像一个对象,但它仍然是一个元组。

或者,如果你不想使用命名元组,可以在定义后使用 ? 运算符:

const goToLocation = (coordinates: [number, number, number?]) => {};

向函数传递类型

让我们快速回顾一下我们之前看到的 Array 类型。

Array<string>;

这个类型描述了一个字符串数组。为了实现这一点,我们将一个类型(string)作为参数传递给另一个类型(Array)。

还有很多其他类型可以接收类型,比如 Promise<string>Record<string, string> 等等。在它们每一个中,我们都使用尖括号将一个类型传递给另一个类型。

但我们也可以使用该语法将类型传递给函数。

向 Set 传递类型

Set 是一个 JavaScript特性,表示一个唯一值的集合。

要创建一个 Set,请使用 new 关键字并调用 Set

const formats = new Set();
        // const formats: Set<unknown>

如果我们将鼠标悬停在 formats 变量上,我们可以看到它的类型是 Set<unknown>

这是因为 Set 不知道它应该是什么类型!我们没有给它传递任何值,所以它默认为 unknown 类型。

让 TypeScript 知道我们希望 Set 持有哪种类型的一种方法是传入一些初始值:

const formats = new Set(["CD", "DVD"]);
        // const formats: Set<string>

在这种情况下,由于我们在创建 Set 时指定了两个字符串,TypeScript 知道 formats 是一个字符串 Set

但并非总是如此,我们创建 Set 时就知道要传递哪些确切的值。我们可能想创建一个空的 Set,我们知道它稍后会持有字符串。

为此,我们可以使用尖括号语法将类型传递给 Set

const formats = new Set<string>();

现在,formats 知道它是一个字符串集合,添加除字符串以外的任何内容都会失败:

formats.add("Digital");
formats.add(8); // 类型 'number' 的参数不能赋给类型 'string' 的参数。2345

这是 TypeScript 中一件非常重要的事情需要理解。你可以将类型以及值传递给函数。

并非所有函数都能接收类型

TypeScript 中的大多数函数不能接收类型。

例如,让我们看看来自 DOM 类型定义的 document.getElementById

一个你可能想传递类型的常见例子是调用 document.getElementById。这里我们试图获取一个音频元素:

const audioElement = document.getElementById("player");

我们知道 audioElement 将是一个 HTMLAudioElement,所以看起来我们应该能够将它传递给 document.getElementById

const audioElement = document.getElementById<HTMLAudioElement>("player"); //期望 0 个类型参数,但获得了 1 个。2558

但不幸的是,我们不能。我们得到一个错误,说 .getElementById 期望零个类型参数。

我们可以通过将鼠标悬停在函数上来查看它是否可以接收类型参数。让我们尝试悬停 .getElementById

// 悬停在 .getElementById 上显示:
(method) Document.getElementById(elementId: string): HTMLElement | null

注意 .getElementById 的悬停信息中不包含尖括号 (<>),这就是为什么我们不能向它传递类型。

让我们将其与一个可以接收类型参数的函数进行对比,比如 document.querySelector

const audioElement = document.querySelector("#player");

// 悬停在 .querySelector 上显示:
(method) ParentNode.querySelector<Element>(selectors: string): Element | null

这个类型定义告诉我们 .querySelector 在圆括号之前有一些尖括号。尖括号内部是它们的默认值——在这种情况下是 Element

所以,为了修复我们上面的代码,我们可以用 .querySelector 替换 .getElementById,并使用 #player 选择器来找到音频元素:

const audioElement = document.querySelector<HTMLAudioElement>("#player");

一切正常。

所以,要判断一个函数是否可以接收类型参数,将鼠标悬停在它上面,检查它是否有任何尖括号。

练习

练习 1:向 Map 传递类型

这里我们正在创建一个 Map,一个表示字典的 JavaScript 特性。

在这种情况下,我们想为键传入一个数字,为值传入一个对象:

const userMap = new Map();
userMap.set(1, { name: "Max", age: 30 });
userMap.set(2, { name: "Manuel", age: 31 });

// @ts-expect-error
// 未使用的 '@ts-expect-error' 指令。2578
userMap.set("3", { name: "Anna", age: 29 });

// @ts-expect-error
// 未使用的 '@ts-expect-error' 指令。2578
userMap.set(3, "123");

@ts-expect-error 指令上有红线,因为当前 Map 中允许任何类型的键和值。

// 悬停在 Map 上显示:
var Map: MapConstructor
new () => Map<any, any> (+3 个重载)

我们如何类型化 userMap,使得键必须是数字,值是具有 nameage 属性的对象?

练习 1:向 Map 传递类型

练习 2:JSON.parse() 不能接收类型参数

考虑以下代码,它使用 JSON.parse 来解析一些 JSON:

const parsedData = JSON.parse<{ // 期望 0 个类型参数,但获得了 1 个。2558
  name: string;
  age: number;
}>('{"name": "Alice", "age": 30}');

目前 JSON.parse 的类型参数下有一个错误。

一个检查 parsedData 类型的测试目前失败,因为它被类型化为 any 而不是期望的类型:

type test = Expect<
  Equal< // 类型 'false' 不满足约束 'true'。2344
    typeof parsedData,
    {
      name: string;
      age: number;
    }
  >
>;

我们试图向 JSON.parse 函数传递一个类型参数。但在这种情况下它似乎不起作用。

测试错误告诉我们 parsed 的类型不是我们期望的。属性 nameage 没有被识别。

为什么会这样?有什么不同的方法可以纠正这些类型错误?

练习 2:JSON.parse() 不能接收类型参数

解决方案 1:向 Map 传递类型

解决这个问题有几种不同的方法,但我们将从最直接的一种开始。

首先要做的是创建一个 User 类型:

type User = {
  name: string;
  age: number;
};

遵循我们目前看到的模式,我们可以将 numberUser 作为 Map 的类型传递:

const userMap = new Map<number, User>();

没错——有些函数可以接收多个类型参数。在这种情况下,Map 构造函数可以接收两种类型:一种用于键,一种用于值。

通过此更改,错误消失了,我们再也不能向 userMap.set 函数传入不正确的类型。

你也可以内联表达 User 类型:

const userMap = new Map<number, { name: string; age: number }>();
解决方案 2:JSON.parse() 不能接收类型参数

让我们更仔细地看看向 JSON.parse 传递类型参数时得到的错误消息:

期望 0 个类型参数,但获得了 1 个。

此消息表明 TypeScript 在调用 JSON.parse 时不期望尖括号内有任何内容。要解决此错误,我们可以移除尖括号:

const parsedData = JSON.parse('{"name": "Alice", "age": 30}');

现在 .parse 接收到正确数量的类型参数,TypeScript 很高兴。

然而,我们希望解析后的数据具有正确的类型。将鼠标悬停在 JSON.parse 上,我们可以看到它的类型定义:

JSON.parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any

它总是返回 any,这有点问题。

要解决这个问题,我们可以给 parsedData 一个变量类型注解,其中 name: stringage: number

const parsedData: {
  name: string;
  age: number;
} = JSON.parse('{"name": "Alice", "age": 30}');

现在我们将 parsedData 类型化为我们想要的样子。

这之所以有效,是因为 any禁用了类型检查。所以,我们可以给它赋任何我们想要的类型。我们可以给它赋一些没有意义的东西,比如 number,TypeScript 也不会抱怨:

const parsedData: number = JSON.parse('{"name": "Alice", "age": 30}');

所以,这更像是“类型信仰”而不是“类型安全”。我们希望 parsedData 是我们期望的类型。这依赖于我们保持类型注解与实际数据同步。

函数类型化

可选参数

对于函数参数是可选的情况,我们可以在 : 前面添加 ? 运算符。

假设我们想给 logAlbumInfo 函数添加一个可选的 releaseDate 参数。我们可以这样做:

const logAlbumInfo = (
  title: string,
  trackCount: number,
  isReleased: boolean,
  releaseDate?: string,
) => {
  // 函数体的其余部分
};

现在我们可以调用 logAlbumInfo 并包含一个发布日期字符串,或者省略它:

logAlbumInfo("Midnights", 13, true, "2022-10-21");
logAlbumInfo("American Beauty", 10, true);

在 VS Code 中将鼠标悬停在可选的 releaseDate 参数上,会显示它现在被类型化为 string | undefined

我们稍后会更多地讨论 | 符号,但这表示该参数可以是 stringundefined。字面上将 undefined 作为第二个参数传递是可以接受的,或者也可以完全省略它。

默认参数

除了将参数标记为可选之外,你还可以使用 = 运算符为参数设置默认值。

例如,如果未提供格式,我们可以将 format 默认设置为 "CD"

const logAlbumInfo = (
  title: string,
  trackCount: number,
  isReleased: boolean,
  format: string = "CD",
) => {
  // 函数体的其余部分
};

: string 的注解也可以省略:

const logAlbumInfo = (
  title: string,
  trackCount: number,
  isReleased: boolean,
  format = "CD",
) => {
  // 函数体的其余部分
};

因为它能从提供的值中推断出 format 参数的类型。这是类型推断的另一个很好的例子。

函数返回类型

除了设置参数类型,我们还可以设置函数的返回类型。

函数的返回类型可以通过在参数列表的右括号后放置一个 : 和类型来进行注解。对于 logAlbumInfo 函数,我们可以指定该函数将返回一个字符串:

const logAlbumInfo = (
  title: string,
  trackCount: number,
  isReleased: boolean,
): string => {
  // 函数体的其余部分
};

如果函数返回的值与指定的类型不匹配,TypeScript 将显示错误。

const logAlbumInfo = (
  title: string,
  trackCount: number,
  isReleased: boolean,
): string => {
  return 123; // 类型 'number' 不能赋值给类型 'string'。2322
};

当你想要确保函数返回特定类型的值时,返回类型非常有用。

剩余参数

就像在 JavaScript 中一样,TypeScript 通过对最后一个参数使用 ... 语法来支持剩余参数。这允许你向函数传递任意数量的参数。

例如,这个 printAlbumFormats 函数被设置为接受一个 album 和任意数量的 formats

function getAlbumFormats(album: Album, ...formats: string[]) {
  return `${album.title} is available in the following formats: ${formats.join(
    ", ",
  )}`;
}

...formats 语法声明参数,并结合一个字符串数组,使我们能够向函数传递任意数量的字符串:

getAlbumFormats(
  { artist: "Radiohead", title: "OK Computer", year: 1997 },
  "CD",
  "LP",
  "Cassette",
);

甚至可以通过展开一个字符串数组来实现:

const albumFormats = ["CD", "LP", "Cassette"];
getAlbumFormats(
  { artist: "Radiohead", title: "OK Computer", year: 1997 },
  ...albumFormats,
);

作为替代方案,我们可以使用 Array<> 语法。

function getAlbumFormats(album: Album, ...formats: Array<string>) {
  // 函数体
}

函数类型

我们已经使用类型注解来指定函数参数的类型,但我们也可以使用 TypeScript 来描述函数本身的类型。

我们可以使用这种语法来做到这一点:

type Mapper = (item: string) => number;

这是一个函数类型别名,该函数接收一个 string 并返回一个 number

然后我们可以用它来描述传递给另一个函数的回调函数:

const mapOverItems = (items: string[], map: Mapper) => {
  return items.map(map);
};

或者,内联声明它:

const mapOverItems = (items: string[], map: (item: string) => number) => {
  return items.map(map);
};

这让我们能够向 mapOverItems 传递一个函数,该函数可以改变数组中项目的值。

const arrayOfNumbers = mapOverItems(["1", "2", "3"], (item) => {
  return parseInt(item) * 100;
});

函数类型与函数定义一样灵活。你可以声明多个参数、剩余参数和可选参数。

// 可选参数
type WithOptional = (index?: number) => number;

// 剩余参数
type WithRest = (...rest: string[]) => number;

// 多个参数
type WithMultiple = (first: string, second: string) => number;

void 类型

有些函数不返回任何东西。它们执行某种操作,但不产生值。

一个很好的例子是 console.log

const logResult = console.log("Hello!");

你期望 logResult 是什么类型?在 JavaScript 中,值是 undefined。如果我们 console.log(logResult),我们会在控制台中看到这个。

但是 TypeScript 为这些情况提供了一种特殊的类型——当一个函数的返回值应该被故意忽略时。它叫做 void

如果我们将鼠标悬停在 console.log 中的 .log 上,我们会看到它返回 void

(method) Console.log(...data: any[]): void

所以,logResult 也是 void

这是 TypeScript 表示“忽略此函数调用的结果”的方式。

类型化异步函数

我们已经了解了如何通过返回类型来强类型化函数返回的内容:

const getUser = (id: string): User => {
  // 函数体
};

但是当函数是异步的时呢?

const getUser = async (id: string): User => { // 一个声明类型既不是 'undefined'、'void'也不是 'any' 的函数必须返回值。2355
                                           // 异步函数或方法的返回类型必须是全局 Promise<T> 类型。你是想写 'Promise<User>' 吗?1064
  // 函数体
};

幸运的是,TypeScript 的错误信息在这里很有帮助。它告诉我们异步函数的返回类型必须是 Promise

所以,我们可以将 User 传递给 Promise

const getUser = async (id: string): Promise<User> => {
  const user = await db.users.get(id);
  return user;
};

现在,我们的函数必须返回一个解析为 UserPromise

练习

练习 1:可选函数参数

这里我们有一个 concatName 函数,其实现接收两个 string 类型的参数 firstlast

如果没有传递 last 姓氏,返回值将仅为 first 名字。否则,它将返回 firstlast 连接的结果:

const concatName = (first: string, last: string) => {
  if (!last) {
    return first;
  }
  return `${first} ${last}`;
};

当使用名字和姓氏调用 concatName 时,函数按预期工作,没有错误:

const result = concatName("John", "Doe");

然而,当仅使用名字调用 concatName 时,我们会得到一个错误:

const result2 = concatName("John"); // 期望 2 个参数,但获得了 1 个。2554

尝试使用可选参数注解来修复错误。

练习 1:可选函数参数

练习 2:默认函数参数

这里我们有和之前一样的 concatName 函数,其中 last 姓氏是可选的:

const concatName = (first: string, last?: string) => {
  if (!last) {
    return first;
  }
  return `${first} ${last}`;
};

我们还有几个测试。这个测试检查当传入名字和姓氏时,函数是否返回全名:

it("应该返回全名", () => {
  const result = concatName("John", "Doe");
  type test = Expect<Equal<typeof result, string>>;
  expect(result).toEqual("John Doe");
});

然而,第二个测试期望当 concatName 仅以名字作为参数调用时,函数应使用 Pocock 作为默认姓氏:

it("应该只返回名字", () => { // 原文是 "should return the first name",但测试用例是 "John Pocock",暗示了默认姓氏,故此处翻译为“应该只返回名字”可能不准确,根据测试用例修改
  const result = concatName("John");
  type test = Expect<Equal<typeof result, string>>;
  expect(result).toEqual("John Pocock");
});

此测试当前失败,vitest 的输出表明错误在 expect 行:

AssertionError: expected 'John' to deeply equal 'John Pocock'
- Expected
+ Received
— John Pocock
+ John
expect(result).toEqual("John Pocock");

更新 concatName 函数,以便在未提供姓氏时使用 Pocock 作为默认姓氏。

练习 2:默认函数参数

练习 3:剩余参数

这里我们有一个 concatenate 函数,它接收可变数量的字符串:

export function concatenate(...strings) { // 剩余参数 'strings' 隐式具有 'any[]' 类型。7019
  return strings.join("");
}

测试通过了,但是 ...strings 剩余参数上有一个错误。

你将如何更新剩余参数以指定它应该是一个字符串数组?

练习 3:剩余参数

练习 4:函数类型

这里,我们有一个 modifyUser 函数,它接收一个 users 数组、我们想要更改的用户的 id,以及一个进行更改的 makeChange 函数:

type User = {
  id: string;
  name: string;
};

const modifyUser = (user: User[], id: string, makeChange) => { // 参数 'makeChange' 隐式具有 'any' 类型。7006
  return user.map((u) => {
    if (u.id === id) {
      return makeChange(u);
    }
    return u;
  });
};

目前 makeChange 下方有一个错误。

以下是此函数如何被调用的示例:

const users: User[] = [
  { id: "1", name: "John" },
  { id: "2", name: "Jane" },
];

modifyUser(users, "1", (user) => { // 参数 'user' 隐式具有 'any' 类型。7006
  return { ...user, name: "Waqas" };
});

在上面的示例中,传递给错误函数的 user 参数也具有“隐式 any”错误。

modifyUser 函数中 makeChange 函数的类型注解需要更新。它应该返回一个修改后的用户。例如,我们不应该能够返回一个 name123,因为在 User 类型中,name 是一个 string

modifyUser(
  users,
  "1",
  // @ts-expect-error
  (user) => {
    return { ...user, name: 123 };
  },
);

你将如何把 makeChange 类型化为一个接收 User 并返回 User 的函数?

练习 4:函数类型

练习 5:返回 void 的函数

这里我们探讨一个经典的 Web 开发示例。

我们有一个 addClickEventListener 函数,它接收一个监听器函数并将其添加到文档中:

const addClickEventListener = (listener) => { // 参数 'listener' 隐式具有 'any' 类型。7006
  document.addEventListener("click", listener);
};

addClickEventListener(() => {
  console.log("Clicked!");
});

目前 listener 下方有一个错误,因为它没有类型签名。

当我们向 addClickEventListener 传递一个不正确的值时,我们没有得到错误。

addClickEventListener(
  // @ts-expect-error
  // 未使用的 '@ts-expect-error' 指令。2578
  "abc",
);

这触发了我们的 @ts-expect-error 指令。

应该如何类型化 addClickEventListener 以便解决每个错误?

练习 5:返回 void 的函数

练习 6:void vs undefined

我们有一个函数,它接受一个回调并调用它。回调不返回任何东西,所以我们将其类型化为 () => undefined

const acceptsCallback = (callback: () => undefined) => {
  callback();
};

但是当我们尝试传入 returnString(一个确实返回某些东西的函数)时,我们得到了一个错误:

const returnString = () => {
  return "Hello!";
};

acceptsCallback(returnString);
// 类型 '() => string' 的参数不能赋给类型 '() => undefined' 的参数。
//   类型 'string' 不能赋值给类型 'undefined'2345

为什么会发生这种情况?我们能改变 acceptsCallback 的类型来修复这个错误吗?

练习 6:void vs undefined

练习 7:类型化异步函数

这个 fetchData 函数等待 fetch 调用的 response,然后通过调用 response.json() 获取 data

async function fetchData() {
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  return data;
}

这里有几点值得注意。

将鼠标悬停在 response 上,我们可以看到它的类型是 Response,这是一个全局可用的类型:

// 悬停在 responseconst response: Response;

当悬停在 response.json() 上时,我们可以看到它返回一个 Promise<any>

// 悬停在 response.json() 上
// (method) Response.json(): Promise<any> // 原文为 const response.json(): Promise<any>,根据上下文应为方法类型

如果我们从对 fetch 的调用中移除 await 关键字,返回类型也将变为 Promise<any>

const response = fetch("https://api.example.com/data");
// 悬停在 response 上显示
const response: Promise<any>;

考虑这个 example 及其测试:

const example = async () => {
  const data = await fetchData();
  type test = Expect<Equal<typeof data, number>>; // 类型 'false' 不满足约束 'true'。2344
};

该测试目前失败,因为 data 被类型化为 any 而不是 number

我们如何在不更改对 fetchresponse.json() 的调用的情况下将 data 类型化为数字?

这里有两种可能的解决方案。

练习 7:类型化异步函数

解决方案 1:可选函数参数

通过在参数末尾添加问号 ?,它将被标记为可选:

function concatName(first: string, last?: string) {
  // ...实现
}
解决方案 2:默认函数参数

要在 TypeScript 中添加默认参数,我们会使用同样在 JavaScript 中使用的 = 语法。

在这种情况下,如果未提供值,我们将更新 last 以默认为 "Pocock":

export const concatName = (first: string, last?: string = "Pocock") => { // 参数不能同时有问号和初始值设定项。1015
  return `${first} ${last}`;
};

虽然这通过了我们的运行时测试,但它实际上在 TypeScript 中失败了。

这是因为 TypeScript 不允许我们同时拥有可选参数和默认值。可选性已经由默认值暗示了。

要修复这个错误,我们可以从 last 参数中移除问号:

export const concatName = (first: string, last = "Pocock") => {
  return `${first} ${last}`;
};
解决方案 3:剩余参数

当使用剩余参数时,传递给函数的所有参数都将被收集到一个数组中。这意味着 strings 参数可以被类型化为一个字符串数组:

export function concatenate(...strings: string[]) {
  return strings.join("");
}

或者,当然,使用 Array<> 语法:

export function concatenate(...strings: Array<string>) {
  return strings.join("");
}
解决方案 4:函数类型

让我们首先将 makeChange 参数注解为一个函数。现在,我们指定它返回 any

const modifyUser = (user: User[], id: string, makeChange: () => any) => {
  return user.map((u) => {
    if (u.id === id) {
      return makeChange(u); // 期望 0 个参数,但获得了 1 个。2554
    }
    return u;
  });
};

完成这第一个更改后,当调用 makeChange 时,我们在 u 下方得到一个错误,因为我们说 makeChange 不接收任何参数。

这告诉我们需要向 makeChange 函数类型添加一个参数。

在这种情况下,我们将指定 user 的类型为 User

const modifyUser = (
  user: User[],
  id: string,
  makeChange: (user: User) => any,
) => {
  // 函数体
};

这已经很好了,但我们还需要确保我们的 makeChange 函数返回一个 User

const modifyUser = (
  user: User[],
  id: string,
  makeChange: (user: User) => User,
) => {
  // 函数体
};

现在错误已解决,并且在编写 makeChange 函数时,我们可以对 User 属性进行自动补全。

或者,我们可以通过为 makeChange 函数类型创建一个类型别名来稍微清理一下代码:

type MakeChangeFunc = (user: User) => User;
const modifyUser = (user: User[], id: string, makeChange: MakeChangeFunc) => {
  // 函数体
};

两种技术的行为相同,但如果你需要重用 makeChange 函数类型,类型别名是更好的选择。

解决方案 5:返回 void 的函数

让我们首先将 listener 参数注解为一个函数。现在,我们指定它返回一个字符串:

const addClickEventListener = (listener: () => string) => {
  document.addEventListener("click", listener);
};

问题是,当我们用一个不返回任何东西的函数调用 addClickEventListener 时,现在会出现错误:

addClickEventListener(() => { // 类型 '() => void' 的参数不能赋给类型 '() => string' 的参数。
                          //   类型 'void' 不能赋值给类型 'string'2345
  console.log("Clicked!");
});

错误消息告诉我们 listener 函数返回 void,它不能赋给 string

这表明,我们不应该将 listener 参数类型化为一个返回字符串的函数,而应该将其类型化为一个返回 void 的函数:

const addClickEventListener = (listener: () => void) => {
  document.addEventListener("click", listener);
};

这是告诉 TypeScript 我们不关心 listener 函数返回值的好方法。

解决方案 6:void vs undefined

解决方案是将 callback 的类型更改为 () => void

const acceptsCallback = (callback: () => void) => {
  callback();
};

现在我们可以毫无问题地传入 returnString。这是因为 returnString 返回一个 string,而 void 告诉 TypeScript 在比较它们时忽略返回值。

所以,如果你真的不关心函数的结果,你应该将其类型化为 () => void

解决方案 7:类型化异步函数

你可能会想尝试向 fetch 传递一个类型参数,类似于你对 MapSet 的做法。

然而,将鼠标悬停在 fetch 上,我们可以看到它不接受类型参数:

// @noErrors
const response = fetch<number>("https://api.example.com/data");
//               ^?

我们也无法向 response.json() 添加类型注解,因为它也不接受类型参数:

const data: number = await response.json<number>(); // 期望 0 个类型参数,但获得了 1 个。2558

一种可行的方法是指定 data 是一个 number

const response = await fetch("https://api.example.com/data");
// ---cut---
const data: number = await response.json();

这之所以有效,是因为 data 之前是 any,而 await response.json() 返回 any。所以现在我们是将 any 放入一个需要 number 的位置。

然而,解决这个问题的最佳方法是向函数添加返回类型。在这种情况下,它应该是一个 number

async function fetchData(): number { // 异步函数或方法的返回类型必须是全局 Promise<T> 类型。你是想写 'Promise<number>' 吗?1064
  // 函数体
  return 123;
}

现在 data 被类型化为 number,只是我们的返回类型注解下有一个错误。

所以,我们应该将返回类型更改为 Promise<number>

async function fetchData(): Promise<number> {
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  return data;
}

通过将 number 包装在 Promise<> 中,我们确保在确定类型之前等待 data

想成为 TypeScript 大师吗?

解锁专业精华版

TypeScript Pro Essentials

查看原文上一章 TypeScript 在开发流程中

下一章 联合类型、字面量类型和类型收窄