likes
comments
collection
share

TypeScript 学习指南——对象

作者站长头像
站长
· 阅读数 16

对象字面量 一组键和值 每个键和值都有自己的类型

第 3 章“联合和字面量”详细介绍了联合和字面量类型:使用基本类型(如 boolean )和它们的字面量值(如 true )。这些基本类型只是触及了 JavaScript 代码常用的复杂对象形状的表面。如果 TypeScript 不能表示这些对象,它将非常无法使用。本章将介绍如何描述复杂的对象形状以及 TypeScript 如何检查其可分配性。

对象类型

使用 {...} 语法创建对象字面量时,TypeScript 会根据其属性将其视为新的对象类型或类型形状。该对象类型将具有与对象值相同的属性名称和基本类型。可以使用 value.member 或等效的 value['member'] 语法访问值的属性。

TypeScript 了解以下 poet 变量的类型是具有两个属性的对象类型:类型为 numberborn 和类型为 stringname 。允许访问这些成员,但尝试访问任何其他成员名称将导致该名称不存在的类型错误:

const poet = { 
  born: 1935,
  name: "Mary Oliver",
};


poet['born']; // Type: number
poet.name; // Type: string

poet.end;
// ~~~
// 错误:类型“{ born: number; name: string; }”上不存在属性“end”。

对象类型是 TypeScript 如何理解 JavaScript 代码的核心概念。nullundefined 以外的每个值在其支持类型形状中都有一组成员,因此 TypeScript 必须了解每个值的对象类型才能对其进行类型检查。

声明对象类型

直接从现有对象推断类型很好,但最终您会希望能够显式声明对象的类型。您需要一种方法来将对象形状与满足它的对象分开描述。

可以使用看起来类似于对象字面量的语法来描述对象类型,但使用类型而不是字段值。这与 TypeScript 在有关类型可分配性的错误消息中显示的语法相同。

poetLater 变量与之前的类型相同,其中 name: stringborn: number

let poetLater: {
  born: number;
  name: string;
};
// Ok
poetLater = {
  born: 1935,
  name: "Mary Oliver",
};
poetLater = "Sappho";
// 错误:不能将类型“string”分配给类型“{ born: number; name: string; }”。

对象类型别名

不断写出对象类型,如 { born: number; name: string; } 很快就会变得令人厌烦。更常见的是使用类型别名为每个类型形状分配一个名称。

前面的代码片段可以用 type Poet 重写,它的额外好处是使 TypeScript 的可分配性错误消息更加直接和可读:

type Poet = {
  born: number;
  name: string;
};

let poetLater: Poet;

// Ok
poetLater = {
  born: 1935,
  name: "Sara Teasdale",
};
poetLater = "Emily Dickinson";
// 错误:不能将类型“string”分配给类型“Poet”。

大多数 TypeScript 项目更喜欢使用 interface 关键字来描述对象类型,这是我在第 7 章 “接口”之前不会介绍的功能。别名对象类型和接口几乎相同:本章中的所有内容也适用于接口。

我现在提出这些对象类型,因为了解 TypeScript 如何解释对象字面量是学习 TypeScript 类型系统的重要组成部分。一旦我们在本书的下一节中切换到功能,这些概念仍然很重要。

结构化类型

TypeScript 的类型系统是结构化类型的:这意味着任何恰好满足类型的值都可以用作该类型的值。换句话说,当您声明一个参数或变量是特定的对象类型时,您是在告诉 TypeScript,无论您使用什么对象,它们都需要具有这些属性。

下面的 WithFirstNameWithLastName 别名对象类型都只声明类型为 string 的单个成员。hasBoth 变量恰好同时具有这两个变量(即使它没有显式声明为这样),因此可以将其提供给声明为两种别名对象类型之一的变量:

type WithFirstName = {
  firstName: string;
};

type WithLastName = {
  lastName: string;
};

const hasBoth = {
  firstName: "Lucille",
  lastName: "Clifton",
};

// 正确:hasBoth 包含一个 string 类型的 “firstName”属性
let withFirstName: WithFirstName = hasBoth;

// 正确:hasBoth 包含一个 string 类型的 “lastName”属性
let withLastName: WithLastName = hasBoth;

结构化类型与鸭子类型不同,后者来自短语“如果它看起来像鸭子,嘎嘎叫像鸭子,那可能是鸭子”。

  • 结构化类型是指有一个静态系统检查类型(在 TypeScript 中,是类型检查器)。
  • 鸭子类型是指在运行时使用对象类型之前不检查对象类型。

一句话:JavaScript鸭子类型的,而 TypeScript结构化类型的

使用情况检查

当向用对象类型注解的位置提供值时,TypeScript 将检查该值是否可分配给该对象类型。首先,该值必须具有对象类型的必需属性。如果对象类型所需的任何成员在对象中缺失,TypeScript 将报类型错误。

以下 FirstAndLastNames 对象类型别名要求 firstlast 属性都存在。允许在声明为 FirstAndLastNames 类型的变量中使用包含这两个条件的对象,但没有它们的对象则不能:

type FirstAndLastNames = {
  first: string;
  last: string;
};

// Ok
const hasBoth: FirstAndLastNames = {
  first: "Sarojini",
  last: "Naidu",
};

const hasOnlyOne: FirstAndLastNames = {
  first: "Sappho"
};
// 类型 "{ first: string; }" 中缺少属性 "last",
// 但类型 "FirstAndLastNames" 中需要该属性。

也不允许两者之间的类型不匹配。对象类型指定所需属性的名称以及这些属性应具有的类型。如果对象的属性不匹配,TypeScript 将报告类型错误。

以下 TimeRange 类型要求 start 成员的类型为 DatehasStartString 对象会导致类型错误,因为它的 start 是类型 string

type TimeRange = { 
  start: Date;
};

const hasStartString: TimeRange = {
  start: "1879-02-13",
    // 错误:不能将类型“string”分配给类型“Date”。
};

多余的属性检查

如果变量声明为对象类型,并且其初始值的字段超过其类型描述的字段,Typescript 将报告类型错误。因此,将变量声明为对象类型是一种方式,可以让类型检查器确保它只有该类型的预期字段。

poetMatch 变量具有对象类型中描述的字段,别名为 Poet,而 extraProperty 会导致具有额外属性的类型错误:

type Poet = {
    born: number;
  name: string;
}

// 正确:所有字段匹配“Poet”的预期
const poetMatch: Poet = {
  born: 1928,
  name: "Maya Angelou"
};

const extraProperty: Poet = {
  activity: "walking",
  born: 1935,
  name: "Mary Oliver",
};

  
// 错误:不能将类型“{ activity: string; born: number; name: string; }”
// 分配给类型“Poet”。
//    对象字面量只能指定已知属性,
//    并且“activity”不在类型“Poet”中。

注意,只有在声明为对象类型的位置上创建对象字面量时,才会触发多余的属性检查。提供现有的对象字面量可以绕过多余的属性检查。

以下 extraPropertyButOk 变量不会触发上一个示例的 Poet 类型错误,因为它的初始值恰好在结构上与 Poet 匹配:

const existingObject = {
  activity: "walking",
  born: 1935,
  name: "Mary Oliver",
};

const extraPropertyButOk: Poet = existingObject; // Ok

在任何位置创建新对象时将触发多余的属性检查,该位置希望它与对象类型(正如您将在后面的章节中看到的那样,包括数组成员、类字段和函数参数)匹配。禁止多余的属性是 TypeScript 帮助确保代码干净并完成预期操作的另一种方式。没有在对象类型中声明的多余属性通常是键入错误的属性名称或未使用的代码。

嵌套对象类型

由于 JavaScript 对象可以嵌套为其他对象的成员,因此 TypeScript 对象类型必须能够在类型系统中表示嵌套的对象类型。执行此操作的语法与以前相同,只是使用了 { … } 对象类型,而不是基本类型名称。

Poem 类型声明为其 author 属性具有 firstName: stringlastName: string 的对象。poemMatch 变量可分配给 Poem,因为它与该结构匹配,而 poemMismatch 不行,因为它的 author 属性包含 name 而不是 firstNamelastName

type Poem = {
  author: {
    firstName: string; lastName: string;
  };

  name: string;
};

// Ok
const poemMatch: Poem = {
  author: {
    firstName: "Sylvia", 
    lastName: "Plath",
  },
  name: "Lady Lazarus",
};

const poemMismatch: Poem = {
  author: {
    name: "Sylvia Plath",
  },
  
  // 错误:不能将类型“{ name: string; }”分配
  // 给类型“{ firstName: string; lastName: string; }”。
  //    对象字面量只能指定已知属性,并且“name”
  //    不在类型“{ firstName: string; lastName: string; }”中。
  name: "Tulips",
};

编写 type Poem 的另一种方法是将 author 属性的形状提取到其自己的别名对象类型 Author 。将嵌套类型提取到它们自己的类型别名中也有助于 TypeScript 提供更多类型错误消息。在这个例子中,错误消息可以说是 'Author' 而不是 '{ firstName: string; lastName: string; }' :

type Author = {
  firstName: string; 
  lastName: string;
};

type Poem = {
  author: Author; 
  name: string;
};

const poemMismatch: Poem = {
  author: {
    name: "Sylvia Plath",
  },
  // 错误:不能将类型“{ name: string; }”分配给类型“Author”。
  //    对象字面量只能指定已知属性,
  //    并且“name”不在类型“Author”中。
  name: "Tulips",
};

通常,推荐像这样将嵌套对象类型移动到它们自己的类型名称中,无论是为了更可读的代码还是更可读的 TypeScript 错误消息。

您将在后面的章节中看到对象类型成员可以是其他类型,比如数组和函数。

可选属性

对象类型属性不必在对象中全部必需。可以在类型属性的类型注解 : 之前包含 ?,表示它是可选属性。

在下面示例中,Book 类型只需要 pages 属性,并且可选地允许 author 属性。依附于它的对象可以提供 author,或者只要它们提供 pages 就将其排除在外:

type Book = {
  author?: string; 
  pages: number;
};

// Ok
const ok: Book = {
  author: "Rita Dove", 
  pages: 80,
};

const missing: Book = {
  author: "Rita Dove",
};
// 错误:类型 "{ author: string; }" 中缺少属性 "pages",
// 但类型 "Book" 中需要该属性。

请记住,可选属性与类型恰好在类型联合中包含 undefined 的属性之间存在差异。声明为带有 ? 的可选属性允许不存在。声明为必需属性和 | undefined 必须存在,即使值为 undefined

在声明变量时,可能会跳过以下 Writers 类型中的 editor 属性,因为它的声明中具有 ?author 属性没有 ?,因此它必须存在,即使其值仅为 undefined

type Writers = {
  author: string | undefined; editor?: string;
};

// 正确:author 被设置为 undefined
const hasRequired: Writers = {
  author: undefined,
};

const missingRequired: Writers = {};
//  ~~~~~~~~~~~~~~~
// 错误:类型 "{}" 中缺少属性 "author",
// 但类型 "Writers" 中需要该属性。

第 7 章“接口”将详细介绍其他类型的属性,而第 13 章“配置选项”将描述 TypeScript 对可选属性的严格设置。

对象类型联合

在 TypeScript 代码中,能够描述一个类型是一个或多个不同对象类型(具有稍微不同属性)是合理的。此外,您的代码可能希望能够根据属性值在这些对象类型之间缩小输入范围。

推断的对象类型联合

如果一个变量的初始值可以是多个对象类型之一,TypeScript 将推断其类型为对象类型的联合。该联合类型将为每个可能的对象形状提供一个组成部分。类型的每个可能属性都将出现在每个组成部分中,尽管它们将是 ? 任何没有初始值的可选类型。

poem 值始终具有 string 类型的 name 属性,并且可能有也可能没有 pagesrhymes 属性:

const poem = Math.random() > 0.5
  ? { name: "The Double Image", pages: 7 }
  : { name: "Her Kind", rhymes: true };
// Type:
// {
//    name: string;
//    pages: number;
//    rhymes?: undefined;
// }
// |
// {
//    name: string;
//    pages?: undefined;
//    rhymes: boolean;
// }

poem.name; // string

poem.pages; // number | undefined
poem.rhymes; // booleans | undefined

显式对象类型联合

或者您可以通过显式使用自己的对象类型联合来更明确地说明对象类型。这样做需要编写更多的代码,但优点是您可以更好地控制对象类型。尤其要注意的是,如果值的类型是对象类型的联合,则 TypeScript 的类型系统将只允许访问所有这些联合类型上存在的属性。

poem 变量的以下版本被显式声明为联合类型,该类型始终具有 name 属性以及 pages 属性或 rhymes 属性。允许访问 names,因为它始终存在,但 pagesrhymes 不能保证存在:

type PoemWithPages = {
  name: string; 
  pages: number;
};

type PoemWithRhymes = {
  name: string; 
  rhymes: boolean;
};

type Poem = PoemWithPages | PoemWithRhymes;

const poem: Poem = Math.random() > 0.5
  ? { name: "The Double Image", pages: 7 }
  : { name: "Her Kind", rhymes: true }; 

poem.name; // Ok
poem.pages;
//  ~~~~~~


// 类型“Poem”上不存在属性“pages”。
//    类型“PoemWithRhymes”上不存在属性“pages”

poem.rhymes;
//  ~~~~~~  
// 类型“Poem”上不存在属性“rhymes”。
//	  类型“PoemWithPages”上不存在属性“rhymes”。

限制对可能不存在的对象成员的访问对于代码安全可能是一件好事。如果值可能是多种类型之一,则不保证在所有这些类型上不存在的属性在对象上存在。

正如必须缩小字面量和或基本类型类型的联合以访问并非所有类型构成上都存在的属性一样,您需要缩小这些对象类型联合的范围。

缩小对象类型

如果类型检查器发现只有在联合类型化值包含特定属性时才能运行代码区域,则会将值的类型缩小到仅包含该属性的成分。换句话说,如果您在代码中检查对象的形状,则 TypeScript 的类型缩小将应用于对象。

继续显式键入的 poem 示例,检查 "pages" in poem 是否作为 TypeScript 的类型保护,以表明它是一个 PoemWithPages 。如果 poem 不是 一个PoemWithPages,那么它一定是一个 PoemWithRhymes

if ("pages" in poem) {
    poem.pages; // 正确:“poem” 被缩小为“PoemWithPages”
} else {
    poem.rhymes; // 正确:“poem” 被缩小为“PoemWithRhymes”
}

请注意,TypeScript 不允许像 if(poem.pages) 这样的真实性存在检查。尝试访问可能不存在的对象属性被视为类型错误,即使使用类似于类型保护的方式:

if (poem.pages) { /* ... */ }
//  ~~~~~
// 类型“PoemWithPages | PoemWithRhymes”上不存在属性“pages”
//    类型“PoemWithRhymes”上不存在属性“pages”

区分联合

JavaScript 和 TypeScript 中联合类型化对象的另一种流行形式是让对象上有一个属性指示对象的形状。这种类型形状称为区分联合,其值表示对象的类型的属性是判别式的的。TypeScript 能够为对判别属性进行类型保护的代码执行类型缩小。

例如,这个 Poem 类型描述的对象,它可以是新的 PoemWithPages 类型或新的 PoemWithRhymes 类型,type 属性表示是哪一个类型。如果 poem.type"pages",则 TypeScript 能够推断出 poem 的类型必须是 PoemWithPages。如果没有该类型缩小,则不能保证值上不存在这两个属性:

type PoemWithPages = {
  name: string; 
  pages: number; 
  type: 'pages';
};

type PoemWithRhymes = {
  name: string; 
  rhymes: boolean; 
  type: 'rhymes';
};

type Poem = PoemWithPages | PoemWithRhymes;

const poem: Poem = Math.random() > 0.5
  ? { name: "The Double Image", pages: 7, type: "pages" }
  : { name: "Her Kind", rhymes: true, type: "rhymes" };

if (poem.type === "pages") {
  console.log(`It's got pages: ${poem.pages}` ); // Ok
} else {
  console.log(`It rhymes: ${poem.rhymes}` );
}

poem.type; // Type: 'pages' | 'rhymes'

poem.pages;
//  ~~~~~
// 错误:类型“Poem”上不存在属性“pages”。
//    类型“PoemWithRhymes”上不存在属性“pages”。

区分联合是 TypeScript 中我最喜欢的功能,因为它们可以完美地将常见的优雅 JavaScript 模式与 TypeScript 的类型缩小结合在一起。第 10 章“泛型”及其相关项目将更多地展示使用可区分联合进行泛型数据操作。

交集类型

TypeScript 的 |(联合类型) 表示值的类型可以是两种或多种不同类型之一。正如 JavaScript 运行时的 | 运算符相当于 & 运算符一样,TypeScript 允许同时表示多种类型的类型:& 交集类型。交集类型通常与对象类型别名一起使用,以创建组合多个现有对象类型的新类型。

以下 ArtworkWriting 类型用于形成具有 genrenamepages 属性组合的 WrittenArt 类型:

type Artwork = {
  genre: string;
  name: string;
};

type Writing = {
  pages: number;
  name: string;
};

type WrittenArt = Artwork & Writing;
// 相当于:
// {
//    genre: string;
//    name: string;
//    pages: number;
// }

交集类型可以与联合类型组合,这有时可用于描述一种类型的区分联合。

这个 ShortPoem 类型始终有 author 属性,然后也是 type 属性上的区分联合:

type ShortPoem = { author: string } & (
  | { kigo: string; type: "haiku"; }
  | { meter: number; type: "villanelle"; }
);

// Ok
const morningGlory: ShortPoem = {
  author: "Fukuda Chiyo-ni", 
  kigo: "Morning Glory",
  type: "haiku",
};

const oneArt: ShortPoem = {
  author: "Elizabeth Bishop", 
  type: "villanelle",
};
// 错误:不能将类型“{ author: string; type: "villanelle"; }”
// 分配给类型“ShortPoem”。
//	不能将类型“{ author: string; type: "villanelle"; }”
//	分配给类型“{ author: string; } & { meter: number; type: "villanelle"; }”。
//	  类型 "{ author: string; type: "villanelle"; }" 中缺少属性 "meter",
//	  但类型 "{ meter: number; type: "villanelle"; }" 中需要该属性。

交集类型的危险

交集类型是一个有用的概念,但很容易混淆您自己或 TypeScript 编译器。我建议在使用代码时尽量保持代码简单。

长可分配性错误

当您创建复杂的交集类型(例如与联合类型组合的类型)时,来自 TypeScript 的可分配性错误消息将更难阅读。这将是 TypeScript 的类型系统(以及一般的类型化编程语言)的一个共同主题:代码越复杂,就越难理解来自类型检查器的消息。

以前面代码片段的 ShortPoem 为例,将类型拆分为一系列别名对象类型以允许 TypeScript 打印这些名称会更具可读性:

type ShortPoemBase = { author: string };
type Haiku = ShortPoemBase & { kigo: string; type: "haiku" };
type Villanelle = ShortPoemBase & { meter: number; type: "villanelle" };
type ShortPoem = Haiku | Villanelle;

const oneArt: ShortPoem = {
  author: "Elizabeth Bishop", type: "villanelle",
};
// 不能将类型“{ author: string; type: "villanelle"; }”
// 分配给类型“ShortPoem”。
//   不能将类型“{ author: string; type: "villanelle"; }”分配给类型“Villanelle”。
//     类型 "{ author: string; type: "villanelle"; }" 中缺少属性 "meter",
//     但类型 "{ meter: number; type: "villanelle"; }" 中需要该属性。

Never

交集类型也很容易被滥用并创建一个不可能的类型。基本类型不能作为交集类型的组成部分连接在一起,因为一个值不可能同时是多个基本类型。尝试用 & 将两个基本类型类型放在一起将导致 never 类型,用关键字 never 表示:

type NotPossible = number & string;
// Type: never

never 关键字和类型是编程语言所指的底层类型或空类型。底层类型没有可能的值也无法达到。不能向类型为底层类型的位置提供任何类型:

let notNumber: NotPossible = 0;
//  ~~~~~~~~~
// 错误:不能将类型“number”分配给类型“never”。

let notString: never = "";
//  ~~~~~~~~~
// 错误:不能将类型“string”分配给类型“never”。

大多数 TypeScript 项目很少(如果有的话)使用 never 类型。它偶尔会出现在代码中表示不可能的状态。不过,大多数情况下,这很可能是误用交集类型造成的错误。我会第 15 章 “类型操作”中详细介绍。

总结

在本章中,您扩展了对 TypeScript 类型系统的掌握,以便能够处理对象:

  • TypeScript 如何从对象类型字面量解释类型
  • 描述对象字面量类型,包括嵌套属性和可选属性
  • 使用对象字面量类型联合进行声明、推断和类型缩小
  • 区分联合和区分者
  • 将对象类型与交集类型组合在一起

现在您已经读完了这一章,您最好练习一下学到的东西 https://learningtypescript.com/objects


律师如何声明他们的 TypeScript 类型? “我反对!”


转载自:https://juejin.cn/post/7205131944762048572
评论
请登录