基础类型与注解
深入学习 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;
}
a 和 b 可能是字符串、布尔值或其他任何东西。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 函数开始,它接收两个布尔参数 a 和 b,并返回 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 函数相似。它接收两个参数 a 和 b,并返回一个字符串。
const concatTwoStrings = (a, b) => { // 参数 'b' 隐式具有 'any' 类型。7006 参数 'a' 隐式具有 'any' 类型。7006
return [a, b].join(" ");
};
目前 a 和 b 参数上有错误,它们没有用类型注解。
用 "Hello" 和 "World" 调用 concatTwoStrings 的 result 并检查它是否为 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 的函数,它接受一个类型为 any 的 e。该函数阻止默认的表单提交行为,然后从表单数据创建一个对象并返回它:
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 类型。
如果你来自另一种语言,你可能会尝试使用 int 或 float,但 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 中总是需要注解。
所以,让我们更新函数声明参数,使 a 和 b 都被指定为 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;
我们已经见过 string、number 和 boolean。symbol 类型用于 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 还会禁用像自动补全这样的有用功能,这可以帮助你避免拼写错误。
没错——上面代码中的错误是由创建 FormData 时 e.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 时,它会为你提供一个自动补全下拉列表,其中包含 name、type 和 age 属性的建议。
这个功能可以节省你很多时间,并且在你有多个名称相似的属性时,也有助于避免拼写错误。
练习
练习 1:对象字面量类型
这里我们有一个 concatName 函数,它接受一个带有 first 和 last 键的 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.first 和 user.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 下方。
为了修复这些错误,我们需要将 first 和 last 属性添加到类型注解中。
const concatName = (user: { first: string; last: string }) => {
return `${user.first} ${user.last}`;
};
现在 TypeScript 知道 user 的 first 和 last 属性都是字符串,并且测试通过了。
解决方案 2:可选属性类型
与我们将函数参数设置为可选类似,我们可以使用 ? 来指定对象的属性是可选的。
正如在之前的练习中所见,我们可以向函数参数添加问号,使其成为可选的:
function concatName(user: { first: string; last?: string }) {
// 实现
}
添加 ?: 向 TypeScript 表明该属性不必存在。
如果我们将鼠标悬停在函数体内的 last 属性上,我们会看到 last 属性是 string | undefined:
// 悬停在 `user.last` 上
(property) last?: string | undefined
这意味着它是 string 或 undefined。这是 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 可以是 string 或 number。
使用类型别名是确保类型定义有单一真实来源的好方法,这使得将来进行更改更加容易。
###跨模块共享类型
类型别名可以在它们自己的 .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);
};
getRectangleArea 和 getRectanglePerimeter 函数都接收一个具有 width 和 height 属性的 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 关键字创建一个具有 width 和 height 属性的 Rectangle 类型:
type Rectangle = {
width: number;
height: number;
};
创建类型别名后,我们可以更新 getRectangleArea 和 getRectanglePerimeter 函数以使用 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 的类型别名,它当前有一个类型为 string 的 userId 属性。
processCart 函数接收一个类型为 ShoppingCart 的 cart 参数。它的实现目前不重要。
重要的是当我们调用 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: "...",
});
该函数被调用时传入一个包含 title、instructions 和 ingredients 属性的对象,但目前存在错误,因为 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 函数接收一个坐标数组。每个坐标都有 latitude 和 longitude,它们都是数字,以及一个可选的 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,使得键必须是数字,值是具有 name 和 age 属性的对象?
练习 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 的类型不是我们期望的。属性 name 和 age 没有被识别。
为什么会这样?有什么不同的方法可以纠正这些类型错误?
练习 2:JSON.parse() 不能接收类型参数
解决方案 1:向 Map 传递类型
解决这个问题有几种不同的方法,但我们将从最直接的一种开始。
首先要做的是创建一个 User 类型:
type User = {
name: string;
age: number;
};
遵循我们目前看到的模式,我们可以将 number 和 User 作为 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: string 和 age: 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。
我们稍后会更多地讨论 | 符号,但这表示该参数可以是 string 或 undefined。字面上将 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;
};
现在,我们的函数必须返回一个解析为 User 的 Promise。
练习
练习 1:可选函数参数
这里我们有一个 concatName 函数,其实现接收两个 string 类型的参数 first 和 last。
如果没有传递 last 姓氏,返回值将仅为 first 名字。否则,它将返回 first 与 last 连接的结果:
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 函数的类型注解需要更新。它应该返回一个修改后的用户。例如,我们不应该能够返回一个 name 为 123,因为在 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,这是一个全局可用的类型:
// 悬停在 response 上
const 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。
我们如何在不更改对 fetch 或 response.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 传递一个类型参数,类似于你对 Map 或 Set 的做法。
然而,将鼠标悬停在 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 大师吗?
解锁专业精华版
