五、高级类型

我们已经了解了 TypeScript 中相当多的类型系统。在本章中,我们将继续这一旅程,这一次将深入探讨一些更高级的类型和概念,这些类型和概念将在本书后面帮助我们创建可重用的强类型组件

我们将学习如何组合现有类型以创建联合类型。我们将在第 8 章React-Redux、中发现,这些类型是创建强类型 React-Redux 代码的基础。

我们在第 2 章中简要介绍了 TypeScript 本 3中的新功能,当时我们了解了unknown类型。我们将在本章中更详细地介绍这些。

泛型是一种类型脚本功能,许多库使用它来允许用户使用其库创建强类型应用程序。React 本身在类组件中使用它,允许我们在组件中创建强类型的道具和状态。

重载签名是一个很好的特性,它允许我们使用一个函数来获取不同的参数组合。我们将在本章中学习如何使用这些。

查找和映射类型允许我们从现有类型动态创建新类型。我们将在本章末尾了解所有这些。

在本章中,我们将学习以下主题:

  • 联合类型
  • 类型防护装置
  • 仿制药
  • 重载签名
  • 查找和映射类型

技术要求

在本章中,我们将使用以下技术:

  • TypeScript 游戏场:这是一个位于的网站 http://www.typescriptlang.org/play 这使我们能够在不安装的情况下播放和理解 TypeScript 中的功能。在本章中,我们大部分时间都会用到这个。
  • Visual Studio 代码:我们需要一个编辑器来编写 React 和 TypeScript 代码,这些代码可以从安装 https://code.visualstudio.com/ 网站。我们还需要在 Visual Studio 代码中安装TSLint(由 egamma 编写)和Prettier(由 Esben Petersen 编写)扩展。

All the code snippets in this chapter can be found online at: https://github.com/carlrip/LearnReact17WithTypeScript/tree/master/05-AdvancedTypes.

联合类型

顾名思义,联合类型是我们可以组合在一起形成新类型的类型。联合通常与字符串文字类型一起使用,我们将在第一节中介绍。联合可以用在一种称为区分联合的模式中,我们可以在创建通用和可重用组件时使用该模式

字符串文字类型

字符串文字类型的变量只能指定在字符串文字类型中指定的确切字符串值。

在 TypeScript 游戏场中,让我们看一个示例:

  1. 让我们创建一个名为Control的字符串文字类型,该类型只能设置为"Textbox"字符串:
type Control = "Textbox";
  1. 现在让我们使用Control类型创建一个名为notes的变量,并将其设置为"Textbox"
let notes: Control;
notes = "Textbox";

正如我们所料,TypeScript 编译器对此感到满意。

  1. 现在,让我们将变量设置为不同的值:
notes = "DropDown";

我们得到编译错误类型“DropDown”不可分配给类型“Textbox”:

  1. 与 TypeScript 中的所有其他类型一样,nullundefined也是有效值:
notes = null;
notes = undefined;

字符串文字类型本身没有那么大的用处。但是,它们在联合类型中使用时非常有用,我们将在下一节中介绍。

字符串文字联合类型

字符串文字联合类型是我们将多个字符串文字类型组合在一起的地方。

让我们继续上一个示例,并完成此操作。

  1. 让我们将Control类型增强为字符串文本的并集:
type Control = "Textbox" | "DropDown"

我们使用|将类型合并为联合类型。

  1. 将我们的notes变量设置为"Textbox""DropDown"现在完全有效:
let notes: Control;
notes = "Textbox";
notes = "DropDown";
  1. 让我们扩展我们的Control类型,以包含更多字符串文字:
type Control = "Textbox" | "DropDown" | "DatePicker" | "NumberSlider";
  1. 我们现在可以将notes变量设置为以下任意值:
notes = "DatePicker";
notes = "NumberSlider";

如果我们仔细想想,这真的很有用。我们本可以将notes变量声明为string,但使用它可以包含的特定字符串文本声明它会使其具有超级类型安全性。

判别联合模式

判别联合模式允许我们处理不同联合类型的逻辑。

让我们看一个例子:

  1. 让我们首先创建三个接口来表示文本框、日期选择器和数字滑块:
interface ITextbox {
  control: "Textbox";
  value: string;
  multiline: boolean;
}

interface IDatePicker {
  control: "DatePicker";
  value: Date;
}

interface INumberSlider {
  control: "NumberSlider";
  value: number;
}

它们都有一个名为control的属性,这将是模式中的判别式。

  1. 让我们继续将这些接口组合成一个名为Field的联合类型:
type Field = ITextbox | IDatePicker | INumberSlider;

因此,我们可以从任何类型创建联合类型,而不仅仅是字符串文本。在本例中,我们从三个接口创建了一个联合类型。

  1. 现在我们创建一个函数来初始化Field类型中的值:
function intializeValue(field: Field) {
  switch (field.control) {
    case "Textbox":
      field.value = "";
      break;
    case "DatePicker":
      field.value = new Date();
      break;
    case "NumberSlider":
      field.value = 0;
      break;
    default:
      const shouldNotReach: never = field;
  }
}

我们需要设置的值取决于判别属性control。因此,我们使用了一个switch语句来分支这个属性。

switch语句中的default分支是事情变得有趣的地方。永远不应该到达该分支,因此我们在该分支中放置了一个带有never类型的语句。在接下来的步骤中,我们将看到这样做的价值。

  1. 让我们假设时间已经过去,我们对复选框字段有了新的要求。让我们为此实现一个接口:
interface ICheckbox {
  control: "Checkbox";
  value: boolean;
}
  1. 我们还将此添加到 unionField类型中:
type Field = ITextbox | IDatePicker | INumberSlider | ICheckbox;

我们将立即看到我们的initializeValue函数在never声明上抛出编译错误:

这是非常有价值的,因为never语句确保我们不会忘记为新的复选框需求添加一个代码分支。

  1. 那么,让我们为"Checkbox"字段实现这个额外的分支:
function intializeValue(field: Field) {
  switch (field.control) {
    case "Textbox":
      field.value = "";
      break;
    case "DatePicker":
      field.value = new Date();
      break;
    case "NumberSlider":
      field.value = 0;
      break;
    case "Checkbox":
 field.value = false;
 break;
    default:
      const shouldNotReach: never = field;
  }
}

因此,联合类型允许我们将任何类型组合在一起形成另一个类型。这允许我们创建更严格的类型,特别是在处理字符串时。有区别的联合模式允许我们为联合中的不同类型拥有逻辑分支,never类型帮助我们捕获在向联合类型中添加新类型时需要发生的所有更改。

类型防护装置

类型保护允许我们在条件代码分支中缩小对象的特定类型。当我们需要实现处理联合中特定类型的代码分支时,它们在处理联合类型时非常有用。

在上一节中,当我们实现intializeValue函数时,我们已经使用了类型保护。判别属性control上的switch语句允许我们设置联合中每个类型的值

我们还有其他方法可以实现类型保护。以下各节介绍了不同的方法。

使用 typeof 关键字

typeof关键字是返回表示类型的字符串的 JavaScript 关键字。因此,我们可以在某种条件下使用它来缩小类型。

让我们看一个例子:

  1. 我们有一个联合类型,可以是字符串或字符串数组:
type StringOrStringArray = string | string[];
  1. 我们需要实现一个名为first的函数,该函数接受StringOrStringArray类型的参数并返回一个string
function first(stringOrArray: StringOrStringArray): string {

}
  1. 如果stringOrArraystring,函数需要返回第一个字符;否则,它应返回第一个数组元素:
function first(stringOrArray: StringOrStringArray): string {
  if (typeof stringOrArray === "string") {
    return stringOrArray.substr(0, 1);
  } else {
    return stringOrArray[0];
  }
}

如果将鼠标悬停在第一个分支中的stringOrArray上方,我们会看到该类型已成功缩小为string

如果将鼠标悬停在第二个分支中的stringOrArray上,我们会看到该类型已成功缩小为string[]

  1. 为了检查我们的功能是否正常工作,我们可以添加以下内容:
console.log(first("The"));
console.log(first(["The", "cat"]));

如果我们运行程序,T输出到控制台。

不过,typeof关键字只能用于 JavaScript 类型。为了说明这一点,让我们创建函数的增强版本:

  1. 我们将调用我们的函数firstEnhanced。我们想让第二个分支专门处理string[]类型,并将第三个分支标记为永远无法到达的地方。让我们尝试一下:
function firstEnhanced(stringOrArray: StringOrStringArray): string {
  if (typeof stringOrArray === "string") {
    return stringOrArray.substr(0, 1);
  } else if (typeof stringOrArray === "string[]") { 
    return stringOrArray[0];
  } else {
    const shouldNotReach: never = stringOrArray;
  }
}

TypeScript 编译器对第二个分支不满意:

这条消息给了我们一条线索,说明发生了什么事。JavaScripttypeof关键字与 JavaScript 类型一起工作,分别为stringnumberbooleansymbolundefinedobjectfunction;因此,错误消息中的联合类型组合了这些类型。所以,在我们的第二个分支中,typeof将实际返回"object"

  1. 让我们正确地实现这一点:
function firstEnhanced(stringOrArray: StringOrStringArray): string {
  if (typeof stringOrArray === "string") {
    return stringOrArray.substr(0, 1);
  } else if (typeof stringOrArray === "object") { 
    return stringOrArray[0];
  } else {
    const shouldNotReach: never = stringOrArray;
  }
}

TypeScript 编译器现在又高兴了。

因此,typeof对于 JavaScript 类型的分支非常有用,但对于特定于 TypeScript 的类型并不理想。让我们在下面几节中了解如何弥合这一差距。

使用 instanceof 关键字

instanceof关键字是另一个 JavaScript 关键字。它检查对象是否具有特定的构造函数。它通常用于确定对象是否是类的实例。

让我们看一个例子:

  1. 我们有两类代表PersonCompany
class Person {
  id: number;
  firstName: string;
  surname: string;
}

class Company {
  id: number;
  name: string;
}
  1. 我们还有一个结合了这两个类的联合类型:
type PersonOrCompany = Person | Company;
  1. 我们现在需要编写一个函数,它接受一个PersonCompany并将它们的名称输出到控制台:
function logName(personOrCompany: PersonOrCompany) {
  if (personOrCompany instanceof Person) {
    console.log(`${personOrCompany.firstName} ${personOrCompany.surname}`);
  } else {
    console.log(personOrCompany.name);
  }
}

当使用instanceof时,我们在前面有我们正在检查的变量,在后面有构造函数名(类名)。

如果我们将鼠标悬停在第一个分支中的personOrCompany上方,则得到Person类型:

如果我们将鼠标悬停在第二个分支中的personOrCompany上,则得到Company类型:

因此,instanceof对于缩小类的范围非常有用。然而,我们使用的很多类型脚本都不是 JavaScript 类型或基于类。那么,在这种情况下我们该怎么办?让我们在以下几节中找到答案。

使用 in 关键字

in关键字是另一个 JavaScript 关键字,可用于检查对象中是否存在属性。

让我们使用in关键字实现上一节中的示例:

  1. 这次我们没有为PersonCompany结构提供类,而是提供了接口:
interface IPerson {
  id: number;
  firstName: string;
  surname: string;
}

interface ICompany {
  id: number;
  name: string;
}
  1. 我们再次从PersonCompany结构创建一个联合类型:
type PersonOrCompany = IPerson | ICompany;
  1. 最后,让我们使用in关键字来实现我们的函数:
function logName(personOrCompany: PersonOrCompany) {
 if ("firstName" in personOrCompany) {
  console.log(`${personOrCompany.firstName} ${personOrCompany.surname}`);
 } else {
  console.log(personOrCompany.name);
 }
}

我们在in关键字前用双引号引上属性名,然后是要检查的对象。

如果我们在第一个分支中将鼠标悬停在personOrCompany上方,则得到IPerson类型。如果我们在第二个分支中将鼠标悬停在personOrCompany上方,则得到ICompany类型

所以,in关键字非常灵活。它可以与任何对象一起使用,通过检查属性是否存在来缩小其类型。

我们将在下一节介绍最后一种类型的后卫。

使用用户定义的类型保护

在我们不能使用其他类型防护装置的情况下,我们可以创建自己的防护装置。我们可以通过创建一个返回类型为类型谓词的函数来实现这一点。在本书前面介绍unknown类型时,我们实际上使用了一个用户定义的类型保护。

让我们使用我们自己的类型保护函数实现上两节中的示例:

  1. 我们有相同的接口和联合类型:
interface IPerson {
  id: number;
  firstName: string;
  surname: string;
}

interface ICompany {
  id: number;
  name: string;
}

type PersonOrCompany = IPerson | ICompany;
  1. 那么,让我们实现 type guard 函数,该函数返回对象是否为类型IPerson
function isPerson(personOrCompany: PersonOrCompany): personOrCompany is IPerson {
  return "firstName" in personOrCompany;
}

类型谓词personOrCompanyIPerson帮助类型脚本编译器缩小类型范围。为了确认这一点,将鼠标悬停在第一个分支中的personOrCompany上时,应给出IPerson类型。如果我们将鼠标悬停在第二个分支中的personOrCompany上,则应得到ICompany类型

创建一个用户定义的类型保护比其他方法要多一些工作,但是如果其他方法不起作用,它为我们提供了很多灵活性来处理这种情况。

仿制药

泛型可以应用于函数或整个类。它是一种允许使用者、自己的类型与泛型函数或类一起使用的机制。以下各节将介绍这两个方面的示例。

泛型函数

让我们看一个泛型函数的示例。我们将围绕fetchJavaScript 函数创建一个包装函数,用于从 web 服务获取数据:

  1. 让我们从创建function签名开始:
function getData<T>(url: string): Promise<T> {

}

我们在函数名后面的尖括号中放一个T来表示它是一个泛型函数。我们实际上可以使用任何字母,但是T是常用的。然后我们在签名中使用T,其中类型是泛型的。在我们的示例中,通用位是返回类型,因此我们返回Promise<T>

如果我们想使用箭头函数,这将是:

const getData = <T>(url: string): Promise<T> => {

};
  1. 现在让我们实现我们的功能:
function getData<T>(url: string): Promise<T> {
  return fetch(url).then(response => {
    if (!response.ok) {
      throw new Error(response.statusText);
    }
    return response.json();
  });
}
  1. 最后,让我们使用函数:
interface IPerson {
  id: number;
  name: string;
}

getData<IPerson>("/people/1").then(person => console.log(person));

我们在函数名后的尖括号中传递要在函数中使用的类型。在我们的例子中,它是IPerson

如果我们将鼠标悬停在then回调中的person上方,我们会看到person被正确输入到IPerson

因此,顾名思义,泛型函数是与泛型类型一起工作的函数。上一个示例的另一个实现是使用any作为返回类型,但这不是类型安全的。

泛型类

我们可以使整个类成为泛型。让我们深入了解一个在列表中存储数据的泛型类的示例:

  1. 让我们先定义类,不包含任何内容:
class List<T> {

}

我们通过在类名后加上<T>将该类标记为泛型。

  1. 在类内部,让我们为列表中的数据创建一个private属性:
private data: T[] = [];

我们使用T引用泛型类型。在我们的示例中,我们的data属性是一个数组,它具有声明类的任何类型。

  1. 现在我们添加一个public方法来获取列表中的所有数据:
public getList(): T[] {
  return this.data;
}

我们使用T[]引用泛型数组作为返回类型。

  1. 让我们实现一种向列表中添加项目的方法:
public add(item: T) {
  this.data.push(item);
}

我们使用泛型类型T引用正在传入的数据项。实现只是使用数组push方法将该项添加到我们的private数组中。

  1. 我们还将实现一种从列表中删除项目的方法:
public remove(item: T) {
  this.data = this.data.filter((dataItem: T) => {
    return !this.equals(item, dataItem);
  });
}
private equals(obj1: T, obj2: T) {
  return Object.keys(obj1).every(key => {
    return obj1[key] === obj2[key];
  });
}

我们再次引用正在传入的泛型类型为T的数据项。该实现使用 arraysfilter方法从我们的私有数组中过滤出该项。过滤器谓词使用一个private方法检查两个对象是否相等。

  1. 现在,我们已经实现了泛型列表类,让我们创建一个类型和一些数据来准备使用它:
interface IPerson {
  id: number;
  name: string;
}
const billy: IPerson = { id: 1, name: "Billy" };
  1. 现在,让我们创建泛型类的一个实例:
const people = new List<IPerson>();

我们将要用于类的类型放在类名称后面的尖括号中。

  1. 我们现在可以通过添加和删除billy与类交互:
people.add(billy);
people.remove(billy);
  1. 让我们尝试对列表实例使用不同的类型:
people.add({name: "Sally"});

我们得到了一个编译错误,正如我们所预料的:

  1. 让我们将列表实例中的所有项目保存到一个变量:
const items = people.getList();

如果将鼠标悬停在items变量上,我们会看到类型已正确推断为IPerson[]

因此,泛型类允许我们使用不同类型的类,但仍然保持强类型。

实际上,我们在书的前面使用了泛型类,在书中我们使用道具和状态实现了 React 类组件:

interface IProps { ... }
interface IState { ... }
class App extends React.Component<IProps, IState> {
  ...
}

这里,React.Component类有两个用于道具和状态的通用参数。

因此,泛型是一个非常重要的概念,我们将在本书中大量使用它来创建强类型的 React 组件。

重载签名

重载签名允许使用不同的签名调用函数。此功能可以很好地用于简化库提供给使用者的一组函数。对于一个包含condenseString公共函数和condenseArray的库来说,将其简化为只包含一个公共condense函数不是很好吗?我们将在本节中进行如下操作:

  1. 我们有一个从字符串中删除空格的函数:
function condenseString(string: string): string {
  return string.split(" ").join("");
}
  1. 我们还有一个从数组项中删除空格的函数:
function condenseArray(array: string[]): string[] {
  return array.map(item => item.split(" ").join(""));
}
  1. 我们现在想把这两个函数合并成一个函数。我们可以使用联合类型按如下方式执行此操作:
function condense(stringOrArray: string | string[]): string | string[] {
  return typeof stringOrArray === "string"
    ? stringOrArray.split(" ").join("")
    : stringOrArray.map(item => item.split(" ").join(""));
}
  1. 让我们使用我们的统一函数:
const condensedText = condense("the cat sat on the mat");

输入函数参数时,IntelliSense 提醒我们需要输入字符串或字符串数组:

如果我们将鼠标悬停在condensedText变量上,我们会看到推断的类型是 union 类型:

  1. 是时候添加两个签名重载来提高函数的使用率了:
function condense(string: string): string;
function condense(array: string[]): string[];
function condense(stringOrArray: string | string[]): string | string[] { ... }

我们在主函数签名之前添加函数重载签名。我们在处理字符串时添加了一个重载,在处理字符串数组时添加了第二个重载。

  1. 让我们使用重载函数:
const moreCondensedText = condense("The cat sat on the mat");

当我们输入参数时,我们现在得到了改进的 IntelliSense。我们还可以通过上下箭头滚动两个不同的签名:

如果我们将鼠标悬停在moreCondensedText变量上,我们会看到得到更好的类型推断:

因此,重载签名可以改善使用我们函数的开发人员的体验。它们可以改进智能感知和类型推断。

查找和映射类型

keyof是 TypeScript 中的一个关键字,用于创建对象中所有属性的联合类型。创建的类型称为查找类型。这允许我们根据现有类型的属性动态创建类型。这是一个有用的特性,我们可以使用它针对不同的数据创建通用但强类型的代码

让我们看一个例子:

  1. 我们有以下IPerson接口:
interface IPerson {
  id: number;
  name: string;
}
  1. 让我们使用keyof在此界面上创建一个查找类型:
type PersonProps = keyof IPerson;

如果我们将鼠标悬停在PersonProps类型上,我们会看到已经创建了一个包含"id""name"的联合类型:

  1. 让我们为IPerson添加一个新属性:
interface IPerson {
  id: number;
  name: string;
  age: number
}

如果我们再次将鼠标悬停在PersonProps类型上,我们会看到该类型已自动扩展为包含"age"

因此,PersonProps类型是一种查找类型,因为它查找需要包含的文本。

现在,让我们使用查找类型创建一些有用的内容:

  1. 我们将创建一个Field类,其中包含字段名、标签和默认值:
class Field {
  name: string;
  label: string;
  defaultValue: any;
}
  1. 这是一个开始,但我们可以通过将类设置为泛型,使name更加强类型化:
class Field<T, K extends keyof T> {
  name: K;
  label: string;
  defaultValue: any;
}

我们在类上创建了两个泛型参数。第一个用于包含字段的对象的类型,第二个用于对象内的属性名称

  1. 如果我们创建一个类的实例,可能会更有意义。让我们使用上一个示例中的IPerson并将"id"作为字段名传入:
const idField: Field<IPerson, "id"> = new Field();
  1. 让我们尝试引用一个在IPerson中不存在的属性:
const addressField: Field<IPerson, "address"> = new Field();

我们得到了一个编译错误,正如我们所预料的:

捕捉这样的问题是查找类型的好处,而不是使用string类型

  1. 让我们把注意力转移到Field类中的defaultValue属性上。目前这不是类型安全的。例如,我们可以将idField设置为字符串:
idField.defaultValue = "2";
  1. 让我们解决这个问题,使defaultValue类型安全:
class Field<T, K extends keyof T> {
  name: K;
  label: string;
  defaultValue: T[K];
}

我们正在使用T[K]查找类型。对于idField,这将解析为IPersonid属性的类型,即number

设置idField.defaultValue的代码行现在抛出一个编译错误,正如我们所预期的:

  1. 我们把"2"改为2
idField.defaultValue = 2;

编译错误消失。

因此,在为变量数据类型创建通用组件时,查找类型非常有用。

现在让我们继续讨论映射类型。同样,这些允许我们从现有类型的属性创建新类型。但是,映射类型允许我们通过从现有属性映射属性来具体定义新类型中的属性。

让我们看一个例子:

  1. 首先,让我们创建一个将在下一步中映射的类型:
interface IPerson {
  id: number;
  name: string;
}
  1. 现在,让我们创建一个新版本的interface,其中所有属性都是使用映射类型的readonly
type ReadonlyPerson = { readonly [P in keyof IPerson]: IPerson[P] };

创建地图的重要位是[P in keyof IPerson]。这将遍历IPerson中的所有属性,并将每个属性分配给P以创建类型。因此,在前面的示例中生成的类型如下所示:

type ReadonlyPerson = { 
  readonly id: number
  readonly name: string 
};
  1. 让我们试试看我们的类型是否真的是readonly
let billy: ReadonlyPerson = {
  id: 1,
  name: "Billy"
};
billy.name = "Sally";

正如我们所预料的,当我们试图将readonly属性设置为新值时,会引发编译错误:

所以我们的映射类型起作用了!这个映射类型的一个更通用的版本实际上是在 TypeScript 中作为一个标准类型,并且是Readonly<T>

  1. 现在让我们使用标准的readonly类型:
let sally: Readonly<IPerson> = {
  id: 1,
  name: "sally"
};
  1. 让我们尝试更改readonly中的值:
Sally.name = "Billy";

正如我们预期的那样,将引发编译错误:

如果我们使用 Visual Studio 代码并在Readonly类型上使用 Go to Definition 选项,我们将得到以下结果:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

这与我们的ReadonlyPerson类型非常相似,但IPerson已被泛型类型T取代。

让我们现在开始创建自己的泛型映射类型:

  1. 我们将创建一个映射类型,该类型将生成string类型的现有类型的所有属性:
type Stringify<T> = { [P in keyof T]: string };
  1. 让我们尝试使用映射类型:
let tim: Stringify<IPerson> = {
 id: "1",
 name: "Tim"
};
  1. 让我们尝试将id设置为一个数字:
tim.id = 1

将引发预期的编译错误:

因此,当我们需要基于现有类型的新类型时,映射类型非常有用。除了Readonly<T>之外,TypeScript 中还有相当多的标准映射类型,例如Partial<T>,它创建了一个映射类型,使得所有属性都是可选的。

总结

在本章中,我们学习了 TypeScript 中一些更高级的类型,从联合类型开始。联合类型非常有用,允许我们通过联合现有类型来创建新类型。我们发现,将字符串文本联合在一起可以创建一个比常规的string更具体、更安全的类型。

我们探索了实现类型保护的各种方法。类型保护有助于编译器缩小逻辑分支中的联合类型。在使用unknown类型时,它们也很有用,可以告诉编译器逻辑分支中的类型是什么。

泛型,顾名思义,允许我们创建泛型类型。在详细介绍了这个主题之后,React 组件中 props 和 state 的类型安全性现在变得更有意义了。在本书的其余部分中,我们将继续大量使用泛型类和函数。

我们了解到重载签名允许我们拥有一个具有不同参数和返回类型的函数。现在,我们可以很好地使用此功能来简化在库中公开的公共函数。

我们了解了如何使用查找和映射类型从现有类型属性动态创建新类型。我们现在知道有很多有用的标准类型脚本映射类型,比如Readonly<T>Partial<T>

了解所有这些特性是下一章的重要准备,在下一章中,我们将深入了解使用 React 组件时的一些常见模式。

问题

让我们来看看有关高级类型的一些问题:

  1. 我们有一个interface代表课程结果,如下所示:
interface ICourseMark {
  courseName: string;
  grade: string;
}

我们可以使用这个interface如下:

const geography: ICourseMark = {
  courseName: "Geography",
  grade: "B"
} 

等级只能是 A、B、C 或 D。我们如何在此界面中创建更强类型的grade属性?

  1. 我们使用以下函数验证数字和字符串是否填充了值:
function isNumberPopulated(field: number): boolean {
  return field !== null && field !== undefined;
}

function isStringPopulated(field: string): boolean {
  return field !== null && field !== undefined && field !== "";
}

我们如何将这些组合成一个名为isPopulated的函数,并使用签名重载?

  1. 我们如何用泛型实现更灵活的isPopulated函数版本?
  2. 我们有以下type阶段别名:
type Stages = {
  pending: 'Pending',
  started: 'Started',
  completed: 'Completed',
};
  1. 我们如何通过编程将其转换为'Pending' | 'Started' | 'Completed'联合类型?
  2. 我们有以下联合类型:
type Grade = 'gold' | 'silver' | 'bronze';

我们如何以编程方式创建以下类型:

type GradeMap = {
  gold: string;
  silver: string;
  bronze: string
};

进一步阅读

TypeScript 文档中有一个关于高级类型的重要部分,值得一看:

https://www.typescriptlang.org/docs/handbook/advanced-types.html