如何在TypeScript中创建自定义类型
作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
TypeScript 是 JavaScript 语言的扩展,它使用带有编译时类型检查器的 JavaScript 运行时。 这种组合允许开发人员使用完整的 JavaScript 生态系统和语言功能,同时在其之上添加可选的静态类型检查、枚举、类和接口。
尽管 TypeScript 中预制的 基本类型将涵盖许多用例,但基于这些基本类型创建自己的自定义类型将允许您确保类型检查器验证特定于您的项目的数据结构。 这将减少项目中出现错误的机会,同时还可以更好地记录整个代码中使用的数据结构。
本教程将向您展示如何在 TypeScript 中使用自定义类型,如何将这些类型与联合和交集组合在一起,以及如何使用实用程序类型为自定义类型增加灵活性。 它将引导您完成不同的代码示例,您可以在自己的 TypeScript 环境或 TypeScript Playground 中进行操作,这是一个允许您直接在浏览器中编写 TypeScript 的在线环境。
先决条件
要遵循本教程,您将需要:
- 一个环境,您可以在其中执行 TypeScript 程序以跟随示例。 要在本地计算机上进行设置,您将需要以下内容: 为了运行处理 TypeScript 相关包的开发环境,同时安装了 Node 和 npm(或 yarn)。 本教程使用 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),这也适用。 此外,您需要在您的机器上安装 TypeScript 编译器 (tsc)。 为此,请参阅官方 TypeScript 网站。
- 如果您不想在本地机器上创建 TypeScript 环境,您可以使用官方的 TypeScript Playground 进行操作。
- 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如 解构、剩余运算符 和 导入/导出 。 如果您需要有关这些主题的更多信息,建议阅读我们的 如何在 JavaScript 中编码系列。
- 本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。 这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。 为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。 您还可以在 TypeScript Playground 中尝试这些好处。
本教程中显示的所有示例都是使用 TypeScript 4.2.2 版创建的。
创建自定义类型
在程序具有复杂数据结构的情况下,使用 TypeScript 的基本类型可能无法完全描述您正在使用的数据结构。 在这些情况下,声明自己的类型将帮助您解决复杂性。 在本节中,您将创建可用于描述您需要在代码中使用的任何对象形状的类型。
自定义类型语法
在 TypeScript 中,创建自定义类型的语法是使用 type
关键字后跟类型名称,然后使用类型属性分配给 {}
块。 采取以下措施:
type Programmer = { name: string; knownFor: string[]; };
语法类似于对象文字,其中键是属性的名称,值是该属性应具有的类型。 这定义了一个类型 Programmer
,它必须是一个 object,其中包含一个字符串值的 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 编译器 (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 }
你甚至可以更进一步,直接在这个 company
对象字面量中设置 manager
:
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 中可用的 array 和元组基本类型 ,您可以为应该具有最少元素数量的数组创建自定义类型。 在本节中,您将使用 TypeScript rest operator ...
来执行此操作。
想象一下,您有一个负责合并多个字符串的函数。 此函数将采用单个数组参数。 这个数组必须至少有两个元素,每个元素都应该是字符串。 您可以使用以下内容创建这样的类型:
type MergeStringsArray = [string, string, ...string[]];
MergeStringsArray
类型利用了这样一个事实,即您可以将 rest 运算符与数组类型一起使用,并将其结果用作元组的第三个元素。 这意味着前两个字符串是必需的,但之后的其他字符串元素不是必需的。
如果一个数组的字符串元素少于两个,它将是无效的,如下所示:
const invalidArray: MergeStringsArray = ['some-string']
TypeScript 编译器在检查此数组时将给出错误 2322
:
OutputType '[string]' is not assignable to type 'MergeStringsArray'. Source has 1 element(s) but target requires 2. (2322)
到目前为止,您已经从基本类型的组合中创建了自己的自定义类型。 在下一节中,您将通过将两个或多个自定义类型组合在一起来创建一个新类型。
组合类型
本节将介绍两种组合类型的方法。 这些将使用 联合运算符 传递任何符合一种或另一种类型的数据,并使用 交集运算符 传递满足两种类型中所有条件的数据。
工会
联合是使用 |
(管道)运算符创建的,它表示可以具有联合中任何类型的值。 举个例子:
type ProductCode = number | string
在此代码中,ProductCode
可以是 string
或 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
开头,然后是一个附加字符串。
如果您将无效值传递给您的类型,例如以下 invalidStringValue
:
type StringThatStartsWithGet = `get${string}`; const invalidStringValue: StringThatStartsWithGet = 'something';
TypeScript 编译器会给你错误 2322
:
OutputType '"something"' is not assignable to type '`get${string}`'. (2322)
使用模板字符串创建类型可帮助您根据项目的特定需求自定义类型。 在下一节中,您将尝试类型断言,它为其他无类型数据添加类型。
使用类型断言
any 类型 可以用作任何值的类型,它通常不提供充分利用 TypeScript 所需的强类型。 但有时您可能会得到一些绑定到 any
的变量,这些变量超出了您的控制范围。 如果您使用的外部依赖项不是用 TypeScript 编写的,或者没有可用的 类型声明 ,就会发生这种情况。
如果你想让你的代码在这些场景中是类型安全的,你可以使用类型断言,这是一种将变量类型更改为另一种类型的方法。 通过在变量后添加 as NewType
可以实现类型断言。 这会将变量的类型更改为 as
关键字后指定的类型。
举个例子:
const valueA: any = 'something'; const valueB = valueA as string;
valueA
具有 any
类型,但是,使用 as
关键字,此代码强制 valueB
具有 string
类型。
注意: 要断言 TypeA
的变量具有 TypeB
类型,TypeB
必须是 TypeA
的子类型。 除了 never
之外,几乎所有 TypeScript 类型都是 any
的子类型,包括 unknown
。
实用程序类型
在前面的部分中,您查看了从基本类型创建自定义类型的多种方法。 但有时您不想从头开始创建一个全新的类型。 有时最好使用现有类型的一些属性,甚至创建一个与另一种类型具有相同形状但所有属性都设置为可选的新类型。
使用 TypeScript 提供的现有实用程序类型,所有这些都是可能的。 本节将介绍其中一些实用程序类型; 有关所有可用的完整列表,请查看 TypeScript 手册的 Utility Types 部分。
所有实用程序类型都是 Generic Types,您可以将其视为接受其他类型作为参数的类型。 可以通过使用 <TypeA, TypeB, ...>
语法向其传递类型参数来识别通用类型。
Record<Key, Value>
Record
实用程序类型可用于以比使用之前介绍的索引签名更简洁的方式创建可索引类型。
在您的可索引类型示例中,您具有以下类型:
type Data = { [key: string]: any; };
您可以使用 Record
实用程序类型,而不是像这样的可索引类型:
type Data = Record<string, any>;
Record
泛型的第一个类型参数是每个 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 的更多教程,请查看我们的 How To Code in TypeScript 系列页面。