如何在 TypeScript 中创建自定义类型

作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。

介绍

TypeScriptJavaScript语言的扩展,它使用 JavaScript 的运行时和编译时类型检查器。这种组合允许开发人员使用完整的 JavaScript 生态系统和语言功能,同时还可以在其上添加可选的静态类型检查、枚举、类和接口。

尽管TypeScript 中预制的基本类型将涵盖许多用例,但基于这些基本类型创建您自己的自定义类型将允许您确保类型检查器验证特定于您的项目的数据结构。这将减少项目中出现错误的机会,同时还允许更好地记录整个代码中使用的数据结构。

本教程将向您展示如何在 TypeScript 中使用自定义类型,如何将这些类型与联合和交集组合在一起,以及如何使用实用程序类型为自定义类型增加灵活性。它将引导您完成不同的代码示例,您可以在自己的 TypeScript 环境或TypeScript Playground(一个允许您直接在浏览器中编写 TypeScript 的在线环境)中遵循这些示例

先决条件

要学习本教程,您需要:

本教程中显示的所有示例都是使用 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 中,编辑器将建议该对象所需的字段及其类型,如下面的动画所示:

显示将“name”和“knownFor”键添加到“Programmer”类型的新实例的建议的动画

如果您使用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']
};

注释说明现在将与字段建议一起出现:

带有 TSDoc 注释的代码完成

在创建具有自定义类型的对象时Programmer,如果您将具有意外类型的值分配给任何属性,TypeScript 将引发错误。使用以下代码块,突出显示的行不符合类型声明:

type Programmer = {
  name: string;
  knownFor: string[];
};

const ada: Programmer = {
  name: true,
  knownFor: ['Mathematics', 'Computing', 'First Programmer']
};

TypeScript Compiler( tsc) 将显示错误2322

Output
Type 'boolean' is not assignable to type 'string'. (2322)

如果您省略了您的类型所需的任何属性,如下所示:

type Programmer = {
  name: string;
  knownFor: string[];
};

const ada: Programmer = {
  name: 'Ada Lovelace'
};

TypeScript 编译器会报错2741

Output
Property '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

Output
Type '{ 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

TypeScript 代码自记录

现在您已经完成了一些使用固定数量的属性创建自己的自定义类型的示例,接下来您将尝试向您的类型添加可选属性。

可选属性

使用前面部分中的自定义类型声明,在创建具有该类型的值时不能省略任何属性。但是,在某些情况下,需要可选的属性可以通过带或不带值的类型检查器。在本节中,您将声明这些可选属性。

要将可选属性添加到类型,请将?修饰符添加到属性。使用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]: typeOfValuestypeOfKeystypeOfValues

然后您可以像使用任何其他类型一样正常使用它:

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类型对象必须有一个statusboolean才能通过类型检查器。

现在您可以创建具有不同数量元素的对象,您可以继续学习 TypeScript 中的数组,它可以具有自定义数量或更多的元素。

创建具有多个元素或更多元素的数组

使用TypeScript 中可用数组和元组基本类型,您可以为应该具有最少元素的数组创建自定义类型。在本节中,您将使用 TypeScript rest 运算符 ...来执行此操作。

假设您有一个函数负责合并多个字符串。此函数将采用单个数组参数。这个数组必须至少有两个元素,每个元素都应该是字符串。您可以使用以下内容创建这样的类型:

type MergeStringsArray = [string, string, ...string[]];

MergeStringsArray类型利用了以下事实:您可以将 rest 运算符与数组类型一起使用,并将其结果用作元组的第三个元素。这意味着需要前两个字符串,但不需要后面的其他字符串元素。

如果一个数组少于两个字符串元素,它将是无效的,如下所示:

const invalidArray: MergeStringsArray = ['some-string']

2322检查此数组时,TypeScript 编译器将给出错误

Output
Type '[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;
};

在这种情况下,所有响应都将具有statusisValid属性,但只有用户响应具有附加user字段。要使用交集类型创建特定 API 用户调用的结果响应,请将StatusResponseGetUserResponse类型结合起来

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将在这里传递类型检查器,因为字符串以getthen开头,后跟一个附加字符串。

如果您将无效值传递给您的类型,如下所示invalidStringValue

type StringThatStartsWithGet = `get${string}`;

const invalidStringValue: StringThatStartsWithGet = 'something';

TypeScript 编译器会给你错误2322

Output
Type '"something"' is not assignable to type '`get${string}`'. (2322)

使用模板字符串创建类型可帮助您根据项目的特定需求自定义类型。在下一节中,您将尝试类型断言,它为其他无类型数据添加类型。

使用类型断言

any类型可以作为任何价值,这往往不提供得到充分受益了打字稿所需要的强类型的类型。但有时您可能最终会绑定一些any超出您控制范围的变量如果您使用的外部依赖项不是用 TypeScript 编写的或没有可用的类型声明,则会发生这种情况

如果您想让您的代码在这些场景中是类型安全的,您可以使用类型断言,这是一种将变量的类型更改为另一种类型的方法。通过在变量后面添加可以实现类型断言这会将变量的类型更改为关键字后指定的类型as NewTypeas

以下面的例子为例:

const valueA: any = 'something';

const valueB = valueA as string;

valueA具有 type any,但是,使用as关键字,此代码强制valueB具有 type string

注意:要断言 的变量TypeA具有类型TypeBTypeB必须是 的子类型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 中编码系列页面

觉得文章有用?

点个广告表达一下你的爱意吧 !😁