作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。
介绍
TypeScript是JavaScript语言的扩展,它使用 JavaScript 的运行时和编译时类型检查器。这种组合允许开发人员使用完整的 JavaScript 生态系统和语言功能,同时还可以在其上添加可选的静态类型检查、枚举、类和接口。
尽管TypeScript 中预制的基本类型将涵盖许多用例,但基于这些基本类型创建您自己的自定义类型将允许您确保类型检查器验证特定于您的项目的数据结构。这将减少项目中出现错误的机会,同时还允许更好地记录整个代码中使用的数据结构。
本教程将向您展示如何在 TypeScript 中使用自定义类型,如何将这些类型与联合和交集组合在一起,以及如何使用实用程序类型为自定义类型增加灵活性。它将引导您完成不同的代码示例,您可以在自己的 TypeScript 环境或TypeScript Playground(一个允许您直接在浏览器中编写 TypeScript 的在线环境)中遵循这些示例。
先决条件
要学习本教程,您需要:
- 您可以在其中执行 TypeScript 程序以跟随示例进行操作的环境。要在本地计算机上进行设置,您需要以下内容:
- 这两个节点和NPM(或丝安装才能运行的开发环境,手柄的打字稿相关的软件包)。本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 进行了测试。要在 macOS 或 Ubuntu 18.04 上安装,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。如果您使用的是适用于 Linux的Windows 子系统 (WSL),这也适用。
- 此外,您将需要
tsc
在您的机器上安装TypeScript 编译器 ( )。为此,请参阅TypeScript 官方网站。
- 如果您不想在本地机器上创建 TypeScript 环境,您可以使用官方的TypeScript Playground进行操作。
- 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如解构、rest 运算符和import/exports。如果您需要有关这些主题的更多信息,建议阅读我们的如何在 JavaScript 中编码系列。
- 本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。这对于使用 TypeScript 不是必需的,但确实可以更多地利用 TypeScript 的功能。为了获得这些好处,您可以使用像Visual Studio Code这样的文本编辑器,它开箱即用地完全支持 TypeScript。您还可以在TypeScript Playground 中尝试这些好处。
本教程中显示的所有示例都是使用 TypeScript 4.2.2 版创建的。
创建自定义类型
在程序具有复杂数据结构的情况下,使用 TypeScript 的基本类型可能无法完全描述您正在使用的数据结构。在这些情况下,声明您自己的类型将帮助您解决复杂性。在本节中,您将创建可用于描述您需要在代码中使用的任何对象形状的类型。
自定义类型语法
在 TypeScript 中,创建自定义类型的语法是使用type
关键字,后跟类型名称,然后分配给{}
具有类型属性的块。采取以下措施:
type Programmer = {
name: string;
knownFor: string[];
};
语法类似于对象字面量,其中键是属性的名称,值是该属性应具有的类型。这定义了一个类型Programmer
,该类型必须是一个对象,其name
键保存字符串值,knownFor
键保存字符串数组。
如前面的示例所示,您可以将其;
用作每个属性之间的分隔符。也可以使用逗号, ,
, 或完全省略分隔符,如下所示:
type Programmer = {
name: string
knownFor: string[]
};
使用自定义类型与使用任何基本类型相同。添加双冒号,然后添加您的类型名称:
type Programmer = {
name: string;
knownFor: string[];
};
const ada: Programmer = {
name: 'Ada Lovelace',
knownFor: ['Mathematics', 'Computing', 'First Programmer']
};
该ada
常量现在将通过类型检查器而不会引发错误。
如果您在任何完全支持 TypeScript 的编辑器中编写此示例,例如在 TypeScript Playground 中,编辑器将建议该对象所需的字段及其类型,如下面的动画所示:
如果您使用TSDoc格式(一种流行的 TypeScript 注释文档样式)向字段添加注释,它们也会在代码完成中得到建议。取下面的代码并在注释中解释:
type Programmer = {
/**
* The full name of the Programmer
*/
name: string;
/**
* This Programmer is known for what?
*/
knownFor: string[];
};
const ada: Programmer = {
name: 'Ada Lovelace',
knownFor: ['Mathematics', 'Computing', 'First Programmer']
};
注释说明现在将与字段建议一起出现:
在创建具有自定义类型的对象时Programmer
,如果您将具有意外类型的值分配给任何属性,TypeScript 将引发错误。使用以下代码块,突出显示的行不符合类型声明:
type Programmer = {
name: string;
knownFor: string[];
};
const ada: Programmer = {
name: true,
knownFor: ['Mathematics', 'Computing', 'First Programmer']
};
TypeScript Compiler( tsc
) 将显示错误2322
:
OutputType 'boolean' is not assignable to type 'string'. (2322)
如果您省略了您的类型所需的任何属性,如下所示:
type Programmer = {
name: string;
knownFor: string[];
};
const ada: Programmer = {
name: 'Ada Lovelace'
};
TypeScript 编译器会报错2741
:
OutputProperty 'knownFor' is missing in type '{ name: string; }' but required in type 'Programmer'. (2741)
添加原始类型中未指定的新属性也会导致错误:
type Programmer = {
name: string;
knownFor: string[];
};
const ada: Programmer = {
name: "Ada Lovelace",
knownFor: ['Mathematics', 'Computing', 'First Programmer'],
age: 36
};
在这种情况下,显示的错误是2322
:
OutputType '{ name: string; knownFor: string[]; age: number; }' is not assignable to type 'Programmer'.
Object literal may only specify known properties, and 'age' does not exist in type 'Programmer'.(2322)
嵌套自定义类型
您还可以将自定义类型嵌套在一起。想象一下,您有一个Company
类型,该类型具有一个manager
符合Person
类型的字段。您可以像这样创建这些类型:
type Person = {
name: string;
};
type Company = {
name: string;
manager: Person;
};
然后你可以创建一个这样的类型的值Company
:
const manager: Person = {
name: 'John Doe',
}
const company: Company = {
name: 'ACME',
manager,
}
此代码将通过类型检查器,因为manager
常量适合为该manager
字段指定的类型。请注意,这使用对象属性速记来声明manager
.
您可以省略manager
常量中的类型,因为它与Person
类型具有相同的形状。当您使用与manager
属性类型所期望的形状相同的对象时,TypeScript 不会引发错误,即使它没有明确设置为具有该Person
类型
以下不会抛出错误:
const manager = {
name: 'John Doe'
}
const company: Company = {
name: 'ACME',
manager
}
你甚至可以更进一步,manager
直接在这个company
对象文字中设置:
const company: Company = {
name: 'ACME',
manager: {
name: 'John Doe'
}
};
所有这些场景都是有效的。
如果在支持 TypeScript 的编辑器中编写这些示例,您会发现编辑器将使用可用的类型信息来记录自己。对于前面的示例,只要您打开 的{}
对象字面量manager
,编辑器就会期待一个name
类型为 的属性string
:
现在您已经完成了一些使用固定数量的属性创建自己的自定义类型的示例,接下来您将尝试向您的类型添加可选属性。
可选属性
使用前面部分中的自定义类型声明,在创建具有该类型的值时不能省略任何属性。但是,在某些情况下,需要可选的属性可以通过带或不带值的类型检查器。在本节中,您将声明这些可选属性。
要将可选属性添加到类型,请将?
修饰符添加到属性。使用Programmer
前面部分中的类型,knownFor
通过添加以下突出显示的字符将属性转换为可选属性:
type Programmer = {
name: string;
knownFor?: string[];
};
在这里,您将?
在属性名称之后添加修饰符。这使得 TypeScript 将此属性视为可选属性,并且在您省略该属性时不会引发错误:
type Programmer = {
name: string;
knownFor?: string[];
};
const ada: Programmer = {
name: 'Ada Lovelace'
};
这将通过而不会出错。
现在您知道如何向类型添加可选属性,是时候学习如何创建可以容纳无限数量字段的类型了。
可索引类型
前面的示例表明,如果给定类型的值在声明时未指定这些属性,则不能向该类型的值添加属性。在本节中,您将创建可索引类型,这些类型允许任何数量的字段,只要它们遵循类型的索引签名。
想象一下,您有一个Data
类型来保存该类型的无限数量的属性any
。你可以像这样声明这个类型:
type Data = {
[key: string]: any;
};
在这里,您使用大括号 ( {}
) 中的类型定义块创建一个普通类型,然后添加格式为 的特殊属性,其中是该对象的键应具有的类型,以及这些键的值应具有的类型有。[key: typeOfKeys]: typeOfValues
typeOfKeys
typeOfValues
然后您可以像使用任何其他类型一样正常使用它:
type Data = {
[key: string]: any;
};
const someData: Data = {
someBooleanKey: true,
someStringKey: 'text goes here'
// ...
}
使用可索引类型,您可以分配无限数量的属性,只要它们与索引签名匹配,索引签名是用于描述可索引类型的键和值的类型的名称。在这种情况下,键有string
类型,值有any
类型。
也可以向可索引类型添加始终需要的特定属性,就像使用普通类型一样。在以下突出显示的代码中,您将status
属性添加到您的Data
类型:
type Data = {
status: boolean;
[key: string]: any;
};
const someData: Data = {
status: true,
someBooleanKey: true,
someStringKey: 'text goes here'
// ...
}
这意味着Data
类型对象必须有一个status
带boolean
值的键才能通过类型检查器。
现在您可以创建具有不同数量元素的对象,您可以继续学习 TypeScript 中的数组,它可以具有自定义数量或更多的元素。
创建具有多个元素或更多元素的数组
使用TypeScript 中可用的数组和元组基本类型,您可以为应该具有最少元素的数组创建自定义类型。在本节中,您将使用 TypeScript rest 运算符 ...
来执行此操作。
假设您有一个函数负责合并多个字符串。此函数将采用单个数组参数。这个数组必须至少有两个元素,每个元素都应该是字符串。您可以使用以下内容创建这样的类型:
type MergeStringsArray = [string, string, ...string[]];
该MergeStringsArray
类型利用了以下事实:您可以将 rest 运算符与数组类型一起使用,并将其结果用作元组的第三个元素。这意味着需要前两个字符串,但不需要后面的其他字符串元素。
如果一个数组少于两个字符串元素,它将是无效的,如下所示:
const invalidArray: MergeStringsArray = ['some-string']
2322
检查此数组时,TypeScript 编译器将给出错误:
OutputType '[string]' is not assignable to type 'MergeStringsArray'.
Source has 1 element(s) but target requires 2. (2322)
到目前为止,您已经从基本类型的组合中创建了自己的自定义类型。在下一节中,您将通过将两个或多个自定义类型组合在一起来创建一个新类型。
作曲类型
本节将通过两种方式将类型组合在一起。这些将使用联合运算符传递任何符合一种类型或另一种类型的数据,并使用交集运算符传递满足两种类型中所有条件的数据。
工会
联合是使用|
(pipe) 运算符创建的,该运算符表示可以具有联合中任何类型的值。以下面的例子为例:
type ProductCode = number | string
在此代码中,ProductCode
可以是 astring
或 a number
。以下代码将通过类型检查器:
type ProductCode = number | string;
const productCodeA: ProductCode = 'this-works';
const productCodeB: ProductCode = 1024;
联合类型可以从任何有效 TypeScript 类型的联合创建。
交叉路口
您可以使用交叉类型来创建一个全新的类型,该类型具有所有交叉在一起的类型的所有属性。
例如,假设您有一些始终出现在 API 调用响应中的常见字段,然后是某些端点的特定字段:
type StatusResponse = {
status: number;
isValid: boolean;
};
type User = {
name: string;
};
type GetUserResponse = {
user: User;
};
在这种情况下,所有响应都将具有status
和isValid
属性,但只有用户响应具有附加user
字段。要使用交集类型创建特定 API 用户调用的结果响应,请将StatusResponse
和GetUserResponse
类型结合起来:
type ApiGetUserResponse = StatusResponse & GetUserResponse;
该类型ApiGetUserResponse
将不得不在所有可用的属性StatusResponse
和那些在可用GetUserResponse
。这意味着数据只有在满足两种类型的所有条件时才会通过类型检查器。以下示例将起作用:
let response: ApiGetUserResponse = {
status: 200,
isValid: true,
user: {
name: 'Sammy'
}
}
另一个示例是数据库客户端为包含连接的查询返回的行类型。您将能够使用交集类型来指定此类查询的结果:
type UserRoleRow = {
role: string;
}
type UserRow = {
name: string;
};
type UserWithRoleRow = UserRow & UserRoleRow;
稍后,如果您使用了fetchRowsFromDatabase()
如下函数:
const joinedRows: UserWithRoleRow = fetchRowsFromDatabase()
生成的常量joinedRows
必须有一个role
属性和一个name
都保存字符串值的属性,以便通过类型检查器。
使用模板字符串类型
从 TypeScript 4.1 开始,可以使用模板字符串类型创建类型。这将允许您创建检查特定字符串格式的类型,并为您的 TypeScript 项目添加更多自定义。
要创建模板字符串类型,您使用的语法与创建模板字符串文字时使用的语法几乎相同。但是,您将在字符串模板中使用其他类型而不是值。
想象一下,您想要创建一个传递所有以get
. 您可以使用模板字符串类型来做到这一点:
type StringThatStartsWithGet = `get${string}`;
const myString: StringThatStartsWithGet = 'getAbc';
myString
将在这里传递类型检查器,因为字符串以get
then开头,后跟一个附加字符串。
如果您将无效值传递给您的类型,如下所示invalidStringValue
:
type StringThatStartsWithGet = `get${string}`;
const invalidStringValue: StringThatStartsWithGet = 'something';
TypeScript 编译器会给你错误2322
:
OutputType '"something"' is not assignable to type '`get${string}`'. (2322)
使用模板字符串创建类型可帮助您根据项目的特定需求自定义类型。在下一节中,您将尝试类型断言,它为其他无类型数据添加类型。
使用类型断言
该any
类型可以作为任何价值,这往往不提供得到充分受益了打字稿所需要的强类型的类型。但有时您可能最终会绑定一些any
超出您控制范围的变量。如果您使用的外部依赖项不是用 TypeScript 编写的或没有可用的类型声明,则会发生这种情况。
如果您想让您的代码在这些场景中是类型安全的,您可以使用类型断言,这是一种将变量的类型更改为另一种类型的方法。通过在变量后面添加,可以实现类型断言。这会将变量的类型更改为关键字后指定的类型。as NewType
as
以下面的例子为例:
const valueA: any = 'something';
const valueB = valueA as string;
valueA
具有 type any
,但是,使用as
关键字,此代码强制valueB
具有 type string
。
注意:要断言 的变量TypeA
具有类型TypeB
,TypeB
必须是 的子类型TypeA
。除了 之外never
,几乎所有的 TypeScript 类型都是 的子类型any
,包括unknown
.
实用程序类型
在前面的部分中,您查看了从基本类型创建自定义类型的多种方法。但有时您不想从头开始创建一个全新的类型。有时最好使用现有类型的一些属性,甚至创建与另一种类型具有相同形状但所有属性都设置为可选的新类型。
所有这些都可以使用 TypeScript 可用的现有实用程序类型来实现。本节将介绍其中一些实用程序类型;有关所有可用类型的完整列表,请查看TypeScript 手册的实用程序类型部分。
所有实用程序类型都是Generic Types,您可以将其视为接受其他类型作为参数的类型。可以通过使用<TypeA, TypeB, ...>
语法将类型参数传递给泛型类型来识别泛型类型。
Record<Key, Value>
Record
与使用前面介绍的索引签名相比,实用程序类型可用于以更简洁的方式创建可索引类型。
在您的可索引类型示例中,您具有以下类型:
type Data = {
[key: string]: any;
};
您可以使用Record
实用程序类型而不是像这样的可索引类型:
type Data = Record<string, any>;
Record
泛型的第一个类型参数是 each 的类型key
。在以下示例中,所有键都必须是字符串:
type Data = Record<string, any>
第二个类型参数是每个value
键的类型。以下将允许值是any
:
type Data = Record<string, any>
Omit<Type, Fields>
该Omit
实用程序类型是创建基于另外一个新的类型,同时排除你不想在结果类型的某些属性很有用。
想象一下,您有以下类型来表示数据库中用户行的类型:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
如果在您的代码中检索除一个字段之外的所有字段addressId
,则可以使用Omit
创建一个没有该字段的新类型:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowWithoutAddressId = Omit<UserRow, 'addressId'>;
第一个参数Omit
是新类型所基于的类型。第二个是您要省略的字段。
如果您将鼠标悬停UserRowWithoutAddressId
在代码编辑器中,您会发现它具有该UserRow
类型的所有属性,但您省略了这些属性。
您可以使用字符串联合将多个字段传递给第二个类型参数。假设您还想省略该id
字段,您可以这样做:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowWithoutIds = Omit<UserRow, 'id' | 'addressId'>;
Pick<Type, Fields>
该Pick
实用程序类型是完全相反Omit
的类型。不是说要省略的字段,而是从其他类型指定要使用的字段。
使用UserRow
您之前使用的相同:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
假设您只需email
要从数据库行中选择键。您可以使用以下方法创建这样的类型Pick
:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowWithEmailOnly = Pick<UserRow, 'email'>;
这里的第一个参数Pick
指定了新类型所基于的类型。第二个是您想要包含的键。
这将等同于以下内容:
type UserRowWithEmailOnly = {
email: string;
}
您还可以使用字符串联合选择多个字段:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowWithEmailOnly = Pick<UserRow, 'name' | 'email'>;
Partial<Type>
使用相同的UserRow
示例,假设您想要创建一个与数据库客户端可用于将新数据插入用户表的对象相匹配的新类型,但有一个小细节:您的数据库对所有字段都有默认值,因此您不是需要通过其中任何一个。为此,您可以使用Partial
实用程序类型来选择性地包含基本类型的所有字段。
您现有的类型 ,UserRow
具有所需的所有属性:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
要创建所有属性都是可选的新类型,您可以使用Partial<Type>
实用程序类型,如下所示:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowInsert = Partial<UserRow>;
这和你的UserRowInsert
样子完全一样:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowInsert = {
id?: number | undefined;
name?: string | undefined;
email?: string | undefined;
addressId?: string | undefined;
};
实用程序类型是一个很好的资源,因为与从 TypeScript 中的基本类型创建它们相比,它们提供了一种更快的方式来构建类型。
结论
创建您自己的自定义类型来表示您自己的代码中使用的数据结构,可以为您的项目提供灵活且有用的 TypeScript 解决方案。除了从整体上提高您自己代码的类型安全性之外,将您自己的业务对象作为代码中的数据结构类型将增加代码库的整体文档,并在与团队成员合作时改善您自己的开发人员体验相同的代码库。
有关 TypeScript 的更多教程,请查看我们的如何在 TypeScript 中编码系列页面。