TS文档学习 --- Object types

1,112 阅读6分钟

本篇整理自 TypeScript Handbook 中 「Object Types」 章节。

对象类型(Object types)

对象类型的定义是可以匿名的

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

也可以使用接口进行定义

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

或者通过类型别名

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

属性修饰符(Property Modifiers)

可选属性(Optional Properties)

我们可以在属性名后面加一个 ? 标记表示这个属性是可选的

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

在 JavaScript 中,如果一个属性值没有被设置,我们获取会得到 undefined

这种判断在 JavaScript 中很常见,以至于提供了专门的语法糖

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
  console.log("x coordinate at", xPos); // (parameter) xPos: number
  console.log("y coordinate at", yPos); // (parameter) yPos: number
  // ...
}

但要注意的是注意现在并没有在解构语法里放置类型注解的方式。

这是因为在 JavaScript 中,以下的写法会被认为是为解构变量起别名

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

readonly 属性(readonly Properties)

在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为,但在类型检查的时候,一个标记为 readonly的属性是不能被写入的

interface SomeType {
  readonly prop: string;
}

const foo: SomeType = {
  prop: 'prop'
}

foo.prop = 123 // error

不过使用 readonly 并不意味着一个值就完全是不变的,亦或者说,内部的内容是不能变的。readonly 仅仅表明属性本身是不能被重新写入的

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

const home: Home = {
  resident: {
    name: 'Klaus',
    age: 23
  }
}

home.resident.age = 18 // succcss


home.resident = { // error
  name: 'Alex',
  age: 24
}

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'

索引签名(Index Signatures)

有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征。

这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型

interface StringArray {
  [index: number]: string;
}

一个索引签名的属性类型必须是 string 或者是 number

但数字索引的返回类型一定要是字符索引返回类型的子类型。

这是因为当使用一个数字进行索引的时候,JavaScript 实际上把它转成了一个字符串。

interface Animal {
  name: string;
}
 
interface Dog extends Animal {
  breed: string;
}
 
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
  [x: number]: Animal;
  // 'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
  [x: string]: Dog;
}

尽管字符串索引用来描述字典模式(对象)非常的有效,但也会强制要求所有的属性要匹配索引签名的返回类型。

interface NumberDictionary {
  [index: string]: number;
 
  length: number; // ok
  name: string; // error
	// Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}

然而,如果一个索引签名是属性类型的联合,那各种类型的属性就可以接受了

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok, length is a number
  name: string; // ok, name is a string
}

你也可以设置索引签名为 readonly

interface ReadonlyStringArray {
  readonly [index: number]: string;
}

属性继承(Extending Types)

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

对接口使用 extends关键字允许我们有效的从其他声明过的类型中拷贝成员,并且随意添加新成员。

接口也可以继承多个类型

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

交叉类型(Intersection Types)

TypeScript 也提供了名为交叉类型(Intersection types)的方法,用于合并已经存在的对象类型。

交叉类型的定义需要用到 & 操作符

interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}
 
// 我们连结 Colorful 和 Circle 产生了一个新的类型,新类型拥有 Colorful 和 Circle 的所有成员
type ColorfulCircle = Colorful & Circle;

接口继承与交叉类型(Interfalces vs Intersections)

最原则性的不同就是在于冲突怎么处理,这也是你决定选择那种方式的主要原因

interface Colorful {
  color: string;
}

interface ColorfulSub extends Colorful {
  color: number
}

// Interface 'ColorfulSub' incorrectly extends interface 'Colorful'.
// Types of property 'color' are incompatible.
// Type 'number' is not assignable to type 'string'

使用继承的方式,如果重写类型会导致编译错误,但交叉类型不会

interface Colorful {
  color: string;
}

// 此时color的类型是 never,取得是 string 和 number 的交集
type ColorfulSub = Colorful & {
  color: number
}

泛型对象类型(Generic Object Types)

// 定义
interface Box<Type> {
  contents: Type;
}

// 调用
let box: Box<string>;

Array 类型(The Array Type)

当我们这样写类型 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"));

ReadonlyArray 类型(The ReadonlyArray Type)

ReadonlyArray 是一个特殊类型,它可以描述数组不能被改变。

ReadonlyArray 主要是用来做意图声明。当我们看到一个函数返回 ReadonlyArray

就是在告诉我们不能去更改其中的内容, ReadonlyArray并不会影响运行时

我们可以直接把一个常规数组赋值给 ReadonlyArray

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

TypeScript 也针对 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[]'.
}

最后有一点要注意,就是 ArraysReadonlyArray 并不能双向的赋值

let x: readonly string[] = [];
let y: string[] = [];
 
x = y; // ok
y = x; // error
// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

元组

元组类型是另外一种 Array 类型,当你明确知道数组包含多少个元素,并且每个位置元素的类型都明确知道的时候,就适合使用元组类型

ReadonlyArray 一样,它并不会在运行时产生影响

在元组类型中,你也可以写一个可选属性,但可选元素必须在最后面,而且也会影响类型的 length

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

Tuples 也可以使用剩余元素语法,但必须是 array/tuple 类型

type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...[string, number], number];

有剩余元素的元组并不会设置 length,因为它只知道在不同位置上的已知元素信息

type StringNumberBooleans = [string, number, ...boolean[]];
const a: StringNumberBooleans = ["hello", 1];
console.log(a.length); // (property) length: number

type StringNumberPair = [string, number];
const d: StringNumberPair = ['1', 1];
console.log(d.length); // (property) length: 2

readonly 元组类型(readonly Tuple Types)

元组类型也是可以设置 readonly

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

在大部分的代码中,元组只是被创建,使用完后也不会被修改,所以尽可能的将元组设置为 readonly 是一个好习惯

如果我们给一个数组字面量 const 断言,也会被推断为 readonly 元组类型

let point = [3, 4] as const;
 
function distanceFromOrigin([x, y]: [number, number]) {
  return Math.sqrt(x ** 2 + y ** 2);
}
 
distanceFromOrigin(point);

// 尽管 distanceFromOrigin 并没有更改传入的元素,但函数希望传入一个可变元组。因为 point 的类型被推断为 readonly [3, 4],它跟 [number number] 并不兼容,所以 TypeScript 给了一个报错