TypeScript 4.2 官方手册译文 - 对象类型

·  阅读 606

其它章节的译文:

从类型创建类型
模块

概述

在 JavaScript 中,分组和传递数据的基本方式是通过对象。在 TypeScript 中,我们通过对象类型来表示。

正如我们所见,对象类型可以是匿名的:

function greet(person: { name: string; age: number }) {
  return "Hello " + person.age; 
}
复制代码

或者通过接口来命名:

interface Person {
  name: string;
  age: number;
}
function greet(person: Person) {
  return "Hello " + person.age;
}
复制代码

或者是类型别名:

type Person = {
  name: string;
  age: number;
}
function greet(person: Person) {
  return "Hello " + person.age;
}
复制代码

在上面的三个例子中,我们编写了接受对象的函数,这些对象包含属性 name (必须是字符串)和 age (必须是数字)。

属性修饰词

对象类型中的每个属性都可以指定一些额外的内容:类型、属性是否可选以及属性是否可写。

可选属性

大多数情况下,我们要处理的对象可能有一个属性集。在这种情况下,我们可以通过在属性名称结尾处添加一个问号(?)来将这些属性标记为可选属性。

interface PaintOptions {
  shape: Shape;
  xPos?: number;
  yPos?: number;
}

function paintShape(opts: PaintOptions) {
  // ...
}

const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
复制代码

在这个例子中,xPosyPos 都是可选的。我们可以选择提供它们中的任何一个,所以上面对 paintShape 的调用都是有效的。可选性说的是,如果设置了属性,它最好有一个特定的类型。

interface PaintOptions {
  shape: Shape;
  xPos?: number;
  yPos?: number;
}

function paintShape(opts: PaintOptions) {
  // ...
}

const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
复制代码

我们也可以读取这些属性 -- 但当我们使用 strictNullChecks 时,TypeScript 会告诉我们这些属性可能是 undefined

function paintShape(opts: PaintOptions) {
  let xPos = opts.xPos;
  //  ^ = (property) PaintOptions.xPos?: number | undefined
  let yPos = opts.yPos;
  //  ^ = (property) PaintOptions.yPos?: number | undefined
}
复制代码

在 JavaScript 中,即使属性从未被设置过,我们仍然可以访问它 -- 它只会给我们未定义的值。我们可以专门处理未定义的情况。

function paintShape(opts: PaintOptions) {
  let xPos = opts.xPos === undefined ? 0 : opts.xPos;
  //  ^ = let xPos: number
  let yPos = opts.yPos === undefined ? 0 : opts.yPos;
  //  ^ = let yPos: number
  // ...
}
复制代码

注意,这种为未指定值设置默认值的模式十分常见,JavaScript 有相应的语法支持。

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
  console.log("x coordinate at", xPos);
  //  ^ = var xPos: number
  console.log("y coordinate at", yPos);
  //  ^ = var yPos: number
  // ...
}
复制代码

这里我们对 paintShape 参数 使用了 解构赋值 并且为 xPosyPos 提供了默认值。现在,xPosyPos 肯定存在于 paintShape 中,但对于任何 paintShape 的调用者来说都是可选的。

注意,目前还没有办法在解构模式中添加类型注释。这是因为下面的语法在 JavaScript 中已经有了不同的含义。

function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
  render(shape);
  //  Cannot find name 'shape'. Did you mean 'Shape'?
  render(xPos);
  //  Cannot find name 'xPos'.
}
复制代码

在对象解构模式中,shape: Shape 表示获取属性 shape 并将其重新定义为一个名为 Shape 的变量。同样,xPos: number 创建一个名为 number 的变量,其值基于 xPos 参数。

只读属性

属性也可以被 TypeScript 标记为只读。虽然它不会改变任何运行时行为,但标记为 readonly 的属性在类型检查期间不可写。

interface SomeType {
  readonly prop: string;
}

function doSomething(obj: SomeType) {
  //  We can read from 'obj.prop'.
  console.log(`prop has the value '${obj.prop}'.`);

  // But we can't re-assign it.
  obj.prop = "hello";
  //  Cannot assign to 'prop' because it is a read-only property.
}
复制代码

使用 readonly 修饰词并不一定意味着值是完全不可变的 -- 换句话说,它的内在内容不能被更改。这只是意味着属性本身不能被重写。

interface Home {
  readonly resident: { name: string; age: number };
}

function visitForBirthday(home: Home) {
  // We can read and update properties from 'home.resident'.
  console.log(`Happy birthday ${home.resident.name}!`);
  home.resident.age++;
}

function evict(home: Home) {
  // But we can't write to the 'resident' property itself on a 'Home'.
  home.resident = {
    // Cannot assign to 'resident' because it is a read-only property.
    name: "Victor the Evictor",
    age: 42,
  };
}
复制代码

管理 readonly 潜在的行为很重要。在 TypeScript 开发期间,指明对象应该如何使用的意图是很有用的。TypeScript 在检查两种类型是否兼容时,不会考虑这两种类型的属性是否为 readonly ,所以 readonly 属性也可以通过别名进行更改。

interface Person {
  name: string;
  age: number;
}
interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}
let writablePerson: Person = {
  name: "Person McPersonface",
  age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
复制代码

扩展类型

通常,类型可能是其他类型的更特定版本。例如,我们可能有一个 BasicAddress 类型用于描述发送信件和包裹所需的字段。

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
复制代码

在某些情况下,这就足够了,但如果地址上的建筑有多个单元,地址通常会有一个关联的单元号。我们可以通过 AddressWithUnit 描述。

interface AddressWithUnit {
  name?: string;
  unit: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
复制代码

这样就完成了,但缺点是,当我们的更改纯粹是一种添加行为的时候就必须重复 BasicAddress 中的所有其他字段。相反,我们可以扩展原来的 BasicAddress 类型,只添加 AddressWithUnit 中特有的新字段。

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
  unit: string;
}
复制代码

接口上的 extends 关键字允许我们从其他命名类型复制成员,并添加我们想要的任何新成员。这有助于减少我们必须编写的类型声明模板文件的数量,并指明同一属性的不同声明可能是相关的。例如,AddressWithUnit 不需要重复 street 属性,而且由于 street 来源于 BasicAddress,我们就知道这两种类型以某种方式相关联。

接口也可以从多种类型扩展。

interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
  color: "red",
  radius: 42,
};
复制代码

交集类型

接口允许我们通过扩展其他类型来构建新的类型。TypeScript 提供了另一个称为交集类型的构造,主要用于组合现有的对象类型。

交集类型使用 & 运算符定义。

interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}
type ColorfulCircle = Colorful & Circle;
复制代码

在这里,我们将 ColorfulCircle 相交以生成一个新类型,它包含了 ColorfulCircle 的所有成员。

function draw(circle: Colorful & Circle) {
  console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}
//  okay
draw({ color: "blue", radius: 42 });
//  oops
draw({ color: "red", raidus: 42 });
//  Argument of type '{ color: string; raidus: number; }' is not assignable
//  to parameter of type 'Colorful & Circle'.
//  Object literal may only specify known properties, but 'raidus' does
//  not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
复制代码

接口 vs 交集

我们刚刚研究了两种组合类型的方法,这两种类型很相似,但实际上有细微的不同。对于接口,我们可以使用extends 子句来扩展其他类型,通过交集做到类似的事情并且使用类型别名命名结果。两者之间的主要区别是如何处理冲突,而这种区别通常是你在交集类型和接口之间抉择的主要原因之一。

泛型对象类型

让我们想象一个可以包含任何值的 Box 类型 -- stringsnumbersGiraffes 等等。

interface Box {
  contents: any;
}
复制代码

现在,contents 属性的类型是 any,这是可以的,但可能会导致后续的问题。

我们可以使用 unknown,但这意味着在已经知道 contents 类型的情况下,我们需要进行预防性检查,或者使用容易出错的类型断言。

interface Box {
  contents: unknown;
}
let x: Box = {
  contents: "hello world",
};
//  we could check 'x.contents'
if (typeof x.contents === "string") {
  console.log(x.contents.toLowerCase());
}
//  or we could use a type assertion
console.log((x.contents as string).toLowerCase());
复制代码

一种确保类型安全的方法是为 contents 的每种类型创建不同的 Box 类型。

interface NumberBox {
  contents: number;
}
interface StringBox {
  contents: string;
}
interface BooleanBox {
  contents: boolean;
}
复制代码

但这意味着我们必须创建不同的函数,或者重载函数,来操作这些类型。

function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
  box.contents = newContents;
}
复制代码

这里有太多模板。而且,我们以后可能需要引入新的类型和重载。这是令人沮丧的,因为我们的 box 类型和重载实际上都是一样的。

然而,我们可以创建一个声明了类型参数的 Box 泛型类型

interface Box<Type> {
  contents: Type;
}
复制代码

稍后,当我们引用 Box 时,我们必须给出一个类型参数来代替 Type

let box: Box<string>;
复制代码

可以将 Box 看作是实际类型的模板,其中 Type 是一个会被其他类型替换的占位符。当 TypeScript 遇到Box<string> 时,它会把 Box<Type> 中的所有 Type 都替换成 string ,最终得到类似于 { contents: string } 的东西。换句话说,Box<string> 和我们之前的 StringBox 的一样的。

interface Box<Type> {
  contents: Type;
}
interface StringBox {
  contents: string;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
// ^ = (property) Box<string>.contents: string
let boxB: StringBox = { contents: "world" };
boxB.contents;
// ^ = (property) StringBox.contents: string
复制代码

Box 是可重复使用的,因为 Type 可以用任何东西代替。这意味着,当我们需要一个新类型的 box 时,根本不需要声明一个新的 Box 类型(如果我们想的话,当然可以声明)。

interface Box<Type> {
  contents: Type;
}
interface Apple {
  // ....
}
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;
复制代码

这也意味着我们可以通过使用泛型函数来完全避免重载

function setContents<Type>(box: Box<Type>, newContents: Type) {
  box.contents = newContents;
}
复制代码

值得注意的是,类型别名也可以是泛型的。我们可以定义新的 Box<Type> 接口:

interface Box<Type> {
  contents: Type;
}
复制代码

通过类型别名来代替:

type Box<Type> = {
  contents: Type;
};
复制代码

由于类型别名与接口的不同,它不仅可以描述对象类型,还可以用来编写其他各种各样的泛型助手类型。

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
// ^ = type OneOrManyOrNull<Type> = OneOrMany<Type> | null
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
// ^ = type OneOrManyOrNullStrings = OneOrMany<string> | null
复制代码

过一会儿我们再回过头来给定别名。

Array 类型

泛型对象类型通常是某种容器类型,它独立于它们所包含的元素类型。以这种方式工作的数据结构是非常美妙的,这样它们就可以跨不同的数据类型重用。事实上,我们在这本手册中一直在使用类似的类型:Array 类型。无论什么时候我们写出像 number[]string[] 这样的类型,都只是 Array<number>Array<string> 的简写。

function doSomething(value: Array<string>) {
  // ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
复制代码

和前面的 Box 类型很像,Array 本身是一个泛型类型。

interface Array<Type> {
  /**
   * Gets or sets the length of the array.
   */
  length: number;
  /**
   * Removes the last element from an array and returns it.
   */
  pop(): Type | undefined;
  /**
   * Appends new elements to an array, and returns the new length of the a
   */
  push(...items: Type[]): number;
  // ...
}
复制代码

现代 JavaScript 还提供了其他泛型的数据结构,如 Map<K, V>Set<T>Promise<T> 。由于 MapSetPromise 的行为方式,它们可以处理任何类型的集合。

ReadonlyArray 类型

ReadonlyArray 是一种特殊类型,用于描述不可变更的数组。

function doStuff(values: ReadonlyArray<string>) {
  // We can read from 'values'...
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
  // ...but we can't mutate 'values'.
  values.push("hello!");
  // Property 'push' does not exist on type 'readonly string[]'.
}
复制代码

和属性的 readonly 修饰词十分类似。当我们看到一个返回 ReadonlyArray 类型值的函数,意味着它不打算改变其内容,当我们看到一个使用 ReadonlyArray 类型参数的函数,意味着我们可以传递任意数组给函数而不用担心它会改变其内容。

Array 不同,我们没有可以使用的 ReadonlyArray 构造函数。

new ReadonlyArray("red", "green", "blue");
// 'ReadonlyArray' only refers to a type, but is being used as a value here.
复制代码

然而,我们可以将普通数组赋值给 ReadonlyArray

const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
复制代码

就像 TypeScript 为 Array<Type> 提供的简写语法 Type[] 一样,它也为 ReadonlyArray<Type> 提供了简写语法 readonly Type[]

function doStuff(values: readonly string[]) {
  // We can read from 'values'...
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
  // ...but we can't mutate 'values'.
  values.push("hello!");
  // Property 'push' does not exist on type 'readonly string[]'.
}
复制代码

最后要注意的一点是,与 readonly 属性修饰词不同,常规 ArrayReadonlyArray 之间是不能双向赋值的。

let x: readonly string[] = [];
let y: string[] = [];
x = y;
y = x;
// The type 'readonly string[]' is 'readonly' and cannot be assigned to
// the mutable type 'string[]'.
复制代码

元组类型

元组类型是另一种类型的数组,它确切地知道它包含多少个元素,以及它在特定位置包含哪种类型。

type StringNumberPair = [string, number];
复制代码

这里,StringNumberPair 是字符串和数字的元组类型。和 ReadonlyArray 一样,它在运行时没有表示,但对TypeScript 很重要。对于类型系统,StringNumberPair 描述了其 0 索引包含一个字符串,1 索引包含一个数字的数组。

function doSomething(pair: [string, number]) {
  const a = pair[0];
  // ^ = const a: string
  const b = pair[1];
  // ^ = const b: number
  // ...
}
doSomething(["hello", 42]);
复制代码

如果我们试图索引不存在元素,就会报错:

function doSomething(pair: [string, number]) {
  // ...
  const c = pair[2];
  // Tuple type '[string, number]' of length '2' has no element at index '2'.
}
复制代码

我们也可以使用 JavaScript 的数组解构来解构元组。

function doSomething(stringHash: [string, number]) {
  const [inputString, hash] = stringHash;
  console.log(inputString);
  // ^ = const inputString: string
  console.log(hash);
  // ^ = const hash: number
}
复制代码

除了这些长度检查之外,像这样的简单元组类型等价于一种特定版本的数组,它声明了特定索引位置的属性,并且使用数值字面量类型声明了长度。

interface StringNumberPair {
  // specialized properties
  length: 2;
  0: string;
  1: number;
  // Other 'Array<string | number>' members...
  slice(start?: number, end?: number): Array<string | number>;
}
复制代码

你可能感兴趣的另一件事是,元组可以通过问号(?在元素类型之后)拥有可选元素。可选的元组元素只能出现在末尾,而且还会影响长度的类型。

type Either2dOr3d = [number, number, number?];

function setCoordinate(coord: Either2dOr3d) {
  const [x, y, z] = coord;
  // ^ = const z: number | undefined
  console.log(`Provided coordinates had ${coord.length} dimensions`);
  // ^ = (property) length: 2 | 3
}
复制代码

元组也可以有剩余元素,且必须是数组或是元组类型。

type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
复制代码
  • stringnumberboolean 描述了一个元组,它的前两个元素分别是 stringnumber,但后面可以有任意数量的 boolean
  • StringBooleansNumber 描述一个元组,它的第一个元素是 string,然后是任意数量的 boolean,并以一个 number 结尾。
  • BooleansStringNumber 描述一个元组,它的起始元素为任意数量的 boolean,以一个 string 和一个 number 结束。

带有剩余元素的元组没有设置"length" -- 它只有一组位于不同位置的已知元素。

const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
复制代码

为什么可选元素和剩余元素会派的上用场?它允许 TypeScript 将元组与参数列表对应起来。元组类型可以在剩余参数和参数中使用,所以下面的示例:

function readButtonInput(...args: [string, number, ...boolean[]]) {
  const [name, version, ...input] = args;
  // ...
}
复制代码

等价于:

function readButtonInput(name: string, version: number, ...input: boolean[])
  // ...
}
复制代码

当你希望使用带有剩余参数的可变数量参数,并且需要最少数量的元素,但又不想引入中间变量时,这是很方便的。

readonly 元组类型

关于元组类型的最后一点注意事项 -- 元组类型有 readonly 变体,可以通过在它们前面加上 readonly 修饰词来指定 -- 就像数组简写语法一样。

function doSomething(pair: readonly [string, number]) {
  // ...
}
复制代码

如你所料,TypeScript 中不允许修改只读元组的任何元素。

function doSomething(pair: readonly [string, number]) {
  pair[0] = "hello!";
  // Cannot assign to '0' because it is a read-only property.
}
复制代码

在大多数代码中,创建的元组不会被修改,所以尽可能地将类型注释为只读元组是一个很好的行为。这一点也很重要,因为带有 const 断言的数组字面量将使用只读元组类型进行推断。

let point = [3, 4] as const;
function distanceFromOrigin([x, y]: [number, number]) {
  return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point);
// Argument of type 'readonly [3, 4]' is not assignable to parameter of type //'[number,number]'.
// The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to
// the mutable type '[number, number]'.
复制代码

这里,distanceFromOrigin 从不修改它的元素,但期望接收一个可变元组参数。由于 point 的类型被推断为readonly [3, 4],它与 [number, number] 不匹配,因为该类型不能保证 point 的元素不会发生改变。

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改