持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
欢迎来到我们关于 TypeScript 泛型系列的第二篇文章!在这篇文章中,我们将仔细研究 TypeScript 推理。我们将讨论高级推理、映射和条件类型等主题。
如果您还没有阅读过,您应该阅读本系列的第一部分,深入了解 TypeScript 泛型:第 1 部分 — 泛型介绍以便更好地学习。
TypeScript 中的高级推理
通过将泛型与类型推断相结合,我们可以创建构建在彼此之上的高级类型。通过以这种方式创建新类型,您可以为您的项目创建一个易于维护的健壮类型系统,因为所有子类型都会自动采用其父类型的更改。
一开始你可能会发现高级类型推断的语法有点棘手,但通过一些练习,你应该能够掌握它。
让我们从映射类型开始。
映射类型
映射类型派生自其他类型。当我们将映射类型与泛型结合起来时,我们可以形成强大而灵活的类型定义。
使用创建新类型keyof
就是一个例子。在 第一篇文章 中,我们简要介绍了keyof
. 现在让我们更深入地探索它。
keyof
是一个 TypeScript 运算符,可让您根据其他类型的属性派生新类型。它接受一个输入类型并返回一个具有原始所有属性的新类型。
这是一个例子:
type KeysOfObject<T> = keyof T;
type User = {
id: number;
name: string;
address: string;
}
type UserKeys = KeysOfObject<User>
const accessUser = (user: User, key:UserKeys) => {
return user[key]
}
const user = {
id: 1,
name: "John Doe",
address: "private"
}
accessUser(user, "address");
accessUser(user, "SSN") // ERROR: Argument of type '"SSN"' is not assignable to parameter of type 'keyof User'.
我们keyof
在一个泛型类型KeysOfObject
中使用,我们可以使用它来创建其他映射类型。我们用来根据User
的属性KeysOfObject
创建一个新类型UserKeys
。
accessUser(user, "SSN")
运行得到一个错误。原因是我们使用类型UserKeys
来确保我们的函数只接受现有的用户字段。
创建映射类型是 TypeScript 将类型添加到语言中的一项基本功能。这些类型(如 Partial、Readonly、Pick 等)使创建自定义映射类型变得更加容易。
这些类型在底层使用泛型来处理它们作为输入接收的类型。让我们更详细地介绍这三种类型。
Partial
Partial 用于创建一个所有字段都设置为可选的新类型。它非常适合使用不完整的对象实例,您事先不知道哪些字段可能会丢失:
type User = {
id: number;
name: string;
address: string;
}
function updateUserObj(id: number, body: Partial<User>) {
return makeUpdateAPIRequest(); // 使用部分用户对象作为主体更新用户;
}
由于我们将body
参数设置为 type Partial<User>
,因此我们不必向此函数提供整个用户对象,只需向我们尝试更新的字段提供。
Readonly
Readonly
类型用于创建所有字段设置为只读的新类型。TypeScript 不允许您在初始化后更改此类对象的任何字段值:
type User = {
id: number;
name: string;
address: string;
}
const readonlyUser: Readonly<User> = { id: 0, name:"John Doe", address: "private" };
readonlyUser.address = "public"; //ERROR: Cannot assign to 'address' because it is a read-only property.
在这里,我们创建了一个新的 type Readonly<User>
,其字段与 User 类型相同,但具有只读权限。当我们创建一个新的实例readonlyUser
并尝试修改它时,TypeScript 会抛出一个错误。
Pick
该Pick
类型用于通过指示您希望复制的字段来创建新类型。要选择字段,请将它们作为联合类型传递:
type PickedUser = Pick<User, "id" | "name">
const pickedUser: PickedUser = {
id: 1,
name: "John Doe"
}
条件类型
现在让我们介绍条件类型。我们知道 JavaScript 中的条件表达式:
const myLabel = isEven ? "even" : "odd";
事实证明,我们可以使用相同的语法来定义类型。TypeScript 的条件类型允许我们根据收到的输入类型的值返回不同的类型:
type Cat = { catName: string }
type Dog = { dogName: string }
type BarkOrMeow<T> = T extends Dog ? { barkSound: "Bark!" } : { meowSound: "Meow!" };
type CatSound = BarkOrMeow<Cat>;
上面的代码定义了一个条件类型BarkOrMeow<T>
,它根据输入T
返回一个barkSound
或meowSound
类型。然后我们通过BarkOrMeow
将类型Cat
传递给CatSound
.
即使通过这个简单的示例,您也可以看到 TypeScript 条件类型很好用。
分布式条件类型
在定义条件类型时,我们可以返回多个分布式类型,而不是返回单个类型作为条件语句的一部分。
分布式类型允许您为代码添加另一个级别的灵活性并处理更复杂的边缘情况:
type dateOrNumberOrString<T> = T extends Date ? Date : T extends number ? Date | number : never
function compareValues<T extends Date | number> (value1: T, value2: dateOrNumberOrString<T>) {
// do the comparison
}
在上面的示例中,我们使用分布式条件类型dateOrNumberOrString
来强制我们compareValues
函数的第二个参数的类型。如果value1
是一个 Date
,我们value2
也想成为一个 Date
。如果value1
是数字,我们希望value2
是日期或数字。
条件类型推断
条件类型的一个更复杂的情况是将新类型推断为条件语句的一部分。我们可以使用infer
关键字根据我们收到的输入的某个属性或签名来推断新类型。
如果没有具体的例子,这听起来可能有点过于抽象,所以让我们看一个:
type inferFromFieldType<T> = T extends { id: infer U } ? U : never;
U
在这个例子中,我们从 type 的id
字段推断类型T
。如果T
有该id
属性,TypeScript 会将该属性的类型推断为U
. 然后您可以U
在同一个条件类型语句中使用。
条件类型推断允许创建一个强大的类型检查逻辑,可以处理深度嵌套的对象。
从函数签名进行类型推断
就像我们从对象字段推断类型一样,我们也可以从函数签名推断类型。
我们可以从函数参数和函数返回类型推断类型:
type inferFromFunctionParam<T> = T extends (a: infer F) => void ? F : never;
type inferFromFunctionReturnType<T> = T extends () => infer F ? F : never;
这里我们有两种泛型类型,它们从输入函数类型推断类型。第一个使用函数参数和另一个函数的返回类型。
让我们看一下如何在实际中使用这些类型之一:
type inferFromFunctionParam<T> = T extends (a: infer F) => void ? F : never;
type inferFromFunctionReturnType<T> = T extends () => infer F ? F : never;
function executeFunction<T extends (param: any) => void> (fn: T, arg: inferFromFunctionParam<T>) {
fn(arg);
}
function sampleFunction(param: string) {
// Do something here!
}
executeFunction(sampleFunction, "Hello world");
我们的executeFunction
函数使用了,我们之前在其第二个参数arg
上创建的泛型类型inferFromFunctionParam
。
executeFunction
的第一个参数是一个接受一个参数param
的函数sampleFunction
。
换句话说,如果fn
应该接收一个字符串,TypeScript 确保我们只能将一个字符串作为第二个arg
参数的值传递给executefunction
。
如果我们尝试使用数字调用executeFunction
,TypeScript 会抛出错误:
executeFunction(sampleFunction, 1); //Argument of type 'number' is not assignable to parameter of type 'string'
根据您收到的函数参数的函数签名来强制执行类型的能力允许完全不同级别的类型安全。
结论
在这篇文章中,我们介绍了高级类型推断并将其与泛型相结合,以在其他类型之上构建灵活的类型。通过对泛型和类型推断的深入了解,我们可以确保流经我们应用程序的所有数据都具有强大的类型安全性。
我们对 TypeScript 泛型的深入研究到此结束。