第 4 章 对象

Flying
2022-11-08 / 0 评论 / 144 阅读 / 正在检测是否收录...

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

第 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 对象会导致类型错误,因为它的 startstring 类型:

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”中。

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

在先前示例的 Poet 类型中,extraPropertyButOk 变量不会触发类型错误,因为其初始值在结构上恰好与 Poet 匹配。

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

const extraPropertyButOk: Poet = existingObject; // Ok

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

嵌套对象类型

JavaScript 对象可以作为其他对象的成员而嵌套在其中,因此 TypeScript 的对象类型必须能够在类型系统中表示嵌套的对象类型。为此,使用的语法与之前相同,只是将 { ... } 对象类型放在基本类型的位置。

Poem 类型被声明为一个对象,其 author 属性具有 firstName: stringlastName: stringpoemMatch 变量可以赋值为 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 的属性必须存在,即使其值是 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 变量始终具有 name 属性,类型为 string,可能具有 pagesrhymes 属性,也可能没有:

const poem = Math.random() > 0.5
  ? { name: "The Double Image", pages: 7 }
  : { name: "Her Kind", rhymes: true };
// 类型:
// {
//    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 属性,以及 pagesrhymes 之一。访问 name 是允许的,因为它始终存在,但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 的例子,通过检查 poem 中是否有 "pages" 属性,来判断它是一个 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 类型?
“我反对!”

1

评论 (0)

取消