欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

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

最编程 2024-03-16 16:33:31
...

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

简介

TypeScriptJavaScript语言的一个扩展,它使用JavaScript的运行时与编译时类型检查器。这种组合允许开发者使用完整的JavaScript生态系统和语言特性,同时也在其之上添加了可选的静态类型检查、枚举、类和接口。

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

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

前提条件

要学习本教程,你需要。

  • 一个可以执行TypeScript程序的环境,以便跟随示例。要在你的本地机器上设置这个,你需要以下东西。
    • 同时安装Nodenpm(或yarn),以便运行一个处理TypeScript相关包的开发环境。本教程在Node.js 14.3.0版本和npm 6.14.5版本中进行了测试。要在macOS或Ubuntu 18.04上安装,请按照《如何在macOS上安装Node.js并创建本地开发环境》中的步骤,或者按照《如何在Ubuntu 18.04上安装Node.js》中的《使用PPA安装》部分进行安装。如果你使用Windows Subsystem for Linux (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 ,它必须是一个对象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中,编辑器会提示该对象所期望的字段和它们的类型,如下面的动画所示。

An animation showing suggestions to add the "name" and "knownFor" key to a new instance of the "Programmer" type

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

注释的说明现在会和字段建议一起出现。

Code completion with TSDoc comments

当用自定义类型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

TypeScript Code Self-Documenting

现在你已经经历了一些创建你自己的具有固定数量属性的自定义类型的例子,接下来你将尝试向你的类型添加可选属性。

可选属性

有了前面几节的自定义类型声明,当用该类型创建一个值时,你不能省略任何属性。然而,有一些情况需要可选属性,无论是否有值都可以通过类型检查器。在本节中,你将声明这些可选属性。

要向一个类型添加可选属性,请向该属性添加? 修饰符。使用前几节中的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 类型。

也可以向你的可索引类型添加始终需要的特定属性,就像你可以向普通类型添加一样。在下面突出显示的代码中,你正在向你的Data 类型添加status 属性。

type Data = {
  status: boolean;
  [key: string]: any;
};

const someData: Data = {
  status: true,
  someBooleanKey: true,
  someStringKey: 'text goes here'
  // ...
}

这将意味着一个Data 类型的对象必须有一个status 的键和一个boolean 的值才能通过类型检查器。

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

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

使用TypeScript中的数组和元组基本类型,你可以为数组创建自定义类型,这些数组应该有最小数量的元素。在这一节中,你将使用TypeScript的休息操作符 ... 来实现这一目的。

想象一下,你有一个负责合并多个字符串的函数。这个函数将接受一个单一的数组参数。这个数组必须至少有两个元素,每个元素都应该是字符串。你可以用下面的方法创建一个这样的类型。

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

MergeStringsArray 类型是利用了你可以对数组类型使用rest operator这一事实,并将其结果作为一个元组的第三个元素。这意味着前两个字符串是必须的,但之后的其他字符串元素就不需要了。

如果一个数组的字符串元素少于两个,它将是无效的,就像下面这样。

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;
};

在这种情况下,所有的响应都会有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 将在这里通过类型检查器,因为该字符串以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 的一个子类型。几乎所有的TypeScript类型,除了never ,都是any 的子类型,包括unknown

实用类型

在前面的章节中,你回顾了从基本类型中创建自定义类型的多种方法。但有时你并不想从头开始创建一个全新的类型。有些时候,最好是使用现有类型的一些属性,或者甚至创建一个新的类型,其形状与另一个类型相同,但所有的属性都设置为可选。

所有这些都可以使用TypeScript现有的实用类型。本节将介绍其中一些实用类型;对于所有可用类型的完整列表,请看TypeScript手册的实用类型部分。

所有的工具类型都是_通用类型_,你可以把它看作是一个接受其他类型作为参数的类型。一个通用类型可以通过使用<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系列页面

推荐阅读