TypeScript 中的对象类型、类型推论-对象、复杂的对象

8,608 阅读5分钟

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

对象的类型

image-20210930150053741.png

接下来,我们来看下这样一个例子:

// 定义接口,一般使用首字母大写
interface Person {
    name: string;
    age: number;
}

// 将这个变量约束为这个接口的格式,定义 name 和 age 的时候,也必须要满足接口要求的类型
let alex: Person = {
    name: 'Alex',
    age: 20
}

添加接口的时候,可以使用分号或者逗号分隔每一项,当然也可以什么都不加。

需要注意的是,如果我们的变量缺少某一项属性,那么它就会报错

当然,多余某一项属性,同样也会报错的。

// 缺少某一项属性
interface Person {
    name: string;
    age: number;
}

let alex: Person = {
    name: 'Alex'
}

// 多余某一项属性
interface Person {
    name: string;
    age: number;
}

let alex: Person = {
    name: 'Alex',
    age: 20,
    gender: 'male'
}

但是有些时候,我们希望这样一个接口,它的某些属性是可选的。我们可以加上一个问号。

// ?: 表示为可选的,将这一条属性标为可选属性
interface Person {
    name: string;
    age?: number;
}

let alex: Person = {
    name: 'Alex'
}

有些时候,我们无法无法预知它有什么新的属性。这个时候,我们就需要用到任意属性了。

需要注意的是:

  • 任意属性,必须要包含上面的所有的数据类型,针对上面那个报错,使用联合类型处理
  • propName 可以改成其他名字,比如 x 或者 y
interface Person {
    name: string;
    age?: number;
    // [propName: string]: any; // 正常
    // [propName: string]: string; // 报错
    // 任意属性,必须要包含上面的所有的数据类型,针对上面那个报错,使用联合类型处理
    [propName: string]: string | number;
}

let alex: Person = {
    name: 'Alex',
    gender: 'male'
}

习题:变量 animal 可以声明为哪些类型?

const animal: _____ = {
    weight: 10,
    isMammal: true
}

A. any

B. number | boolean

C. { number | boolean }

D. { weight: number; isMammal: boolean; }

答案

A D

解析

变量 animal 被赋值为对象,因此变量 animal 应该声明为具体的对象类型。

  • A - 任何不确定类型的变量都可以声明为 any,故正确。
  • B - 本选项的联合类型中均为基本数据类型。故错误。
  • C - 错误的写法。
  • D - 本选项为一个对象类型,且结构类型与赋值对象结构类型相符,故正确。

资料:只读属性

在日常的开发过程中,由于多人协作或者项目较为复杂等因素造成对象的值被重写,从而导致 bug 产生的情况时常发生。所以在编码阶段能够检测到此类错误并阻止通过编译显得尤为重要。恰好,TypeScript 为我们提供了这种能力,我们可以用 readonly 定义只读属性:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,使用 readonly 定义的属性 id 初始化后,又被赋值了,所以报错了。

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

tom.id = 89757;

// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,报错信息有两处,第一处是在对 tom 进行赋值的时候,没有给 id 赋值。

第二处是在给 tom.id 赋值的时候,由于它是只读属性,所以报错了。

类型推论 - 对象

接着前面的例子,继续学习类型推论,在对象中是怎样的?

interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let alex: Person = {
    name: 'Alex',
    gender: 'male'
}

把类型定义,直接放到变量定义的地方呢?这样的话,我们就可以省掉 interface

let alex: {
    name: string;
    age?: number;
    [propName: string]: string | number;
} = {
    name: 'Alex',
    gender: 'male'
}

现在 alex 就是后面定义的类型。如果我们把后面定义的这一串类型,全部省掉的话。

这时候的 alex,就会根据它的值,自动推论出类型。

image-20211009221128564.png

复杂的对象

前面我们学习的都是简单的对象,包含一个层级,那如果这个对象,包含多个层级呢?ts 该怎么去描述它?

let alex: Person = {
    name: 'Alex',
    gender: 'male',
    friend: {
        name: 'Lucy',
        age: 18
    }
}

那能不能在 interface 直接这样描述呢?答案是可以的。

interface Person {
    name: string;
    gender: string;
    friend: { // 对象结构
        name: string;
        age: number;
    }
}

// 这样就可以正确描述 alex 类型了
let alex: Person = {
    name: 'Alex',
    gender: 'male',
    friend: {
        name: 'Lucy',
        age: 18,
        // gender: 'female' // 报错
    }
}

习题:在严格类型模式下,变量 animal 的类型

const animal = {
    weight: 10,
    isMammal: true
}

A. any

B. number | boolean

C. { number | boolean }

D. { weight: number; isMammal: boolean; }

答案:D

解析

变量 animal 被赋值为对象,因此变量 animal 会被推论为具体的对象类型。

  • A - any 不是具体的对象类型,故错误。
  • B - 本选项的联合类型中均为基本数据类型。故错误。
  • C - 错误的写法。
  • D - 本选项为一个对象类型,且结构类型与赋值对象结构类型相符,故正确。