# 对象类型-内建对象和字面量对象

本节讨论如何利用 TypeScript 限制内建对象和字面量对象。

# 对象分类

JavaScript 包含大量对象。大体分为三类

  • 内建对象 由 ECMAScript 规范定义
  • 宿主对象 取决于 JS 的执行环境,例如浏览器端的 Window,DOM。node 端的 Process,Global 等
  • 自定义对象 用户定义的对象

这里我们只讨论规范定义的内建对象和自定义对象,后续章节会逐步涉及在不同宿主环境下如何进行类型限制。

# 内建对象

参考 MDN 内建对象 (opens new window) 规范提供了大量的内建对象,你也可以采用类似限制字面量对象的方法限制变量为内建类型。

// 限制对象为 string 的原始封装类型和字符串,不建议使用,此处只是示例说明
let s: String = new String('1');
// 限制对象为 number 的原始封装类型和字符串,不建议使用,此处只是示例说明
let n: Number = new Number(1);

// 限制对象为 Error 的实例
let e: Error = new Error('foo');
e = new TypeError('foo'); // 合法 TypeError 继承自 Error

// 限制对象为 Date 的实例类型
let d: Date = new Date('2019');
// 限制对象为函数类型
let f: Function = () => {};
// 限制对象为 Promise 的实例且返回值必须为 number 类型
let p: Promise<number> = Promise.resolve(1); // 注意这里的尖括号后续章节会有详细描述
// 限制对象为 Set 的实例且元素必须为数值
let setVal: Set<number> = new Set([1, 2, 3]);
// 限制为对象类型
let obj: Object = new Object(1);
obj = s; // 合法
obj = n; // 合法
obj = d; // 合法
obj = f; // 合法
obj = {}; // 合法,此处定义了字面量对象
obj = Object.create(null); // 合法虽然原型链为 null
obj = 12; // 合法
obj = 'foo'; // 合法
obj = null; // 报错
obj = undefined; // 报错

注意上述示例中 Object 类型可以赋值为原始值,为了修正此错误, TypeScript 定义了 object 用于表示非原始类型的所有对象。

let obj1: Object = new Object(1);
obj1 = new Boolean(false); // 合法, Boolean 原始封装对象
obj1 = new Number(10); // 合法 Number 原始封装对象
obj1 = Object(BigInt(1)); // 合法 BigInt 原始封装对象
obj1 = Object(Symbol(1)); // 合法 Symbol 原始封装对象
obj1 = new String('demo'); // 合法 String 原始封装对象
obj1 = {}; // 合法,此处定义了字面量对象
obj1 = Object.create(null); // 合法虽然原型链为 null
// Object 支持原始类型的值
obj1 = false; // 合法
obj1 = 1; // 合法
obj1 = 12n; // 合法
obj1 = 'foo'; // 合法
obj1 = Symbol(1); // 合法

obj1 = undefined; // 报错
obj1 = null; // 报错

let obj2: object = new Object(1);
obj2 = new Boolean(false); // 合法, Boolean 原始封装对象
obj2 = new Number(10); // 合法 Number 原始封装对象
obj2 = Object(BigInt(1)); // 合法 BigInt 原始封装对象
obj2 = Object(Symbol(1)); // 合法 Symbol 原始封装对象
obj2 = new String('demo'); // 合法 String 原始封装对象
obj2 = {}; // 合法,此处定义了字面量对象
obj2 = Object.create(null); // 合法虽然原型链为 null

// object 不支持原始类型的值
obj2 = undefined; // 报错
obj2 = null; // 报错
obj2 = false; // 报错
obj2 = 1; // 报错
obj2 = 12n; // 报错
obj2 = 'foo'; // 报错
obj2 = Symbol(1); // 报错

TIP

还有一个常见的对象类型为 {} 申明效果同 Object

TypeScript 内建了一系列类型来表示内建对象。这些文件定义在 TypeScript lib 目录 (opens new window)。当需要限制变量为特定的内建类型时,你可以通过在 github 搜索这些文件来查看此类型的定义及使用方法。

TIP

TypeScript 以 .d.ts 结尾的文件叫做申明文件用于告知 TypeScript 编译器此类型的定义,避免检查报错。你可查看 申明文件 进一步理解

# 字面量对象

JavaScript 有多种方式创建对象,这里讲解如何限制字面量形式的对象创建。 采用类型注解语法格式为 : { 属性1:类型,属性2:类型 } 限制对象。

// 定义 point 变量,只能接受包含 x,y 两个属性且属性值为 number 类型的对象
let point: {
  x: number;
  y: number;
};

// 合法
point = {
  x: 1,
  y: 2
};

// 报错
point = {
  x: 1,
  y: 'demo' // 必须为 number 类型
};

// 报错
point = { x: 1, y: 2, z: 2 }; // 包含多余参数 z

你也可以采用之前讲解的 类型别名 type 复用创建的对象类型

// 注意属性是采用分号分格而非逗号!
//  TODO: 是否可以采用逗号风格确保和使用功能模式一直
type Obj = {
  foo: number;
  bar: string;
};

let p0: Obj = { foo: 10, bar: 'foo' };

# 属性修饰

# 只读属性

有时我们期望对象的属性的值是不可修改的,可以采用 readonly 表示申明的属性不可修改。

type PointReadOnly = {
  readonly x: number;
  readonly y: number;
};

let p3: PointReadOnly;

// 合法此处属于初始化赋值
p3 = { x: 10, y: 20 };

// 报错 p3 为只读属性无法修改属性值
p3.x = 10;
p3.y = 10;

// 编译为 js 返回 {x:10,y:10}, TypeScript 只进行静态类型检查,不修改运行时特征
console.log(p3);

由于 TypeScript 只进行静态类型检查,如果我们需要限制运行时也无法修改, 推荐阅读 Nicholas C. Zakas 的 JavaScript面向对象精要 第三章 理解属性控制,对象封印等技术。

# 可选属性

除了限制属性为只读,我们也可采用 ?: 的语法表示申明的属性为可选。

type PointOptional = {
  x: number;
  y?: number; // 属性可选
};

let p4: PointOptional;

// 合法 y 属性为可选
p4 = { x: 10 };

// 合法
p4 = { x: 10, y: 10 };

我们也可结合上述语法声明只读可选属性

type PointCombine = {
  readonly x: number; // 只读属性 x
  readonly y?: number; // 只读可选属性 y
  readonly z?: number; // 只读可选属性 z
};

let p5: PointCombine;

// 合法此处属于初始化赋值
p5 = { x: 10 };

// 报错,必须在初始化时赋值不要动态添加属性值
p5.y = 10;
p5.z = 10;

# 索引类型

在 JavaScript 中我们经常会在对象上动态添加属性, 采用 [属性名:类型] 的方式表示此对象可扩展。

type PointDynamic = {
  x: number;
  y: number;
  // 注意此处的索引名没有限制,命名尽量语义化
  [pointName: string]: number;
};

let p6: PointDynamic;

// 合法此处属于初始化赋值
p6 = { x: 10, y: 20 };

// 合法
p6.z = 10;
p6.a = 1;

TIP

索引名称只能为 string 和 number 类型,number 表示类数组结构。 后续会涉及。

# interface

除了使用类型别名 type 表示对象类型,也可采用 interface 语法定义对象类型。

interface LabeledValue {
  label: string;
  size?: number;
}

// 采用接口定义对象类型
let label: LabeledValue;
// 合法
label = { size: 10, label: 'Size 10 Object' };
// 合法
label = { label: 'foo' };

// 报错,缺少 label 属性
label = { size: 10 };

interface a string;

Interface 多用于描述对象类型,而 type 为设定的类型提供了一个别名。Interface 申明会创建一个类型名,用于报错提示而 type 不会。

interface Demo {
  foo: string;
  bar: string;
}

// 为 Demo 类型提供一个别名 D
type D = Demo;

// 当在 vscode 等编辑器中,悬浮在变量 fooBar1 上会显示类型 Demo 而非 D
let fooBar1: D = {
  foo: 'foo',
  bar: 'bar'
};

// 报错,注意观察报错信息指向的类型名是 Demo 而非 D
fooBar1 = {
  foo: 'bar'
};

# 总结

本小节我们学习了 TypeScript 如何限制内建对象和采用字面量模式申明的对象。知识点为:

  1. 利用内建对象的构造器名称限制变量为对应类型的实例,这些类型都定义在 TypeScript lib 目录 (opens new window)
    1. Object 用于限制所有对象且值可以为原始值,object 避免了此问题,取值不能为原始值
  2. 采用 :{属性名1:类型,属性名2:类型,... } 表示对象类型
    1. readonly 显示属性为只读
    2. ?: 表示属性可选
  3. 采用索引类型 [indexName:类型]:类型 表示对象可动态添加属性
    1. indexName 知识一个语义化的符号
    2. indexName 可以为 number 或 string 类型,number 多表示数组结构
  4. 采用 Interface 进行对象类型描述,Interface 相比 type 会创建类型名用于报错提提等。