对象类型

JavaScript 中存在这样一种说法,那就是 一切皆为对象。有这种说法是因为 JavaScript 中的绝大多数值都可以使用对象来表示。例如,函数、数组和对象字面量等本质上都是对象。对于原始数据类型,如 String 类型,JavaScript 也提供了相应的构造函数来创建能够表示原始值的对象。例如,下例中使用内置的 String 构造函数创建了一个表示字符串的对象,示例如下:

const hi = new String('hi');

在某些操作中,原始值还会自动地执行封箱操作,将原始数据类型转换为对象数据类型。例如,在字符串字面量上直接调用内置的 toUpperCase() 方法时,JavaScript 会先将字符串字面量转换为对象类型,然后再调用字符串对象上的 toUpperCase() 方法。示例如下:

// 自动封箱,将'hi'转换为String对象类型
'hi'.toUpperCase();

// 自动封箱,将3转换为Number对象类型
// 注意:这里使用了两个点符号
3..toString()

前面已经介绍过的数组类型、元组类型以及后面章节中将介绍的函数类型、接口等都属于对象类型。由于对象类型的应用非常广泛,因此 TypeScript 提供了多种定义对象类型的方式。在本节中,我们将首先介绍三种基本的对象类型:

  • Object 类型(首字母为大写字母 O)

  • object 类型(首字母为小写字母 o)

  • 对象类型字面量

在后面的章节中,我们会陆续介绍定义对象类型的其他方式。

Object

这里的 Object 指的是 Object 类型,而不是 JavaScript 内置的 Object() 构造函数。请读者一定要注意区分这两者,Object 类型表示一种类型,而 Object() 构造函数则表示一个值。因为 Object() 构造函数是一个值,因此它也有自己的类型。但要注意的是,Object() 构造函数的类型不是 Object 类型。为了更好地理解 Object 类型,让我们先了解一下 Object() 构造函数。

JavaScript 提供了内置的 Object() 构造函数来创建一个对象。示例如下:

const obj = new Object();

在实际代码中,使用 Object() 构造函数来创建对象的方式并不常用。在创建对象时,我们通常会选择使用更简洁的对象字面量。虽然不常使用 Object() 构造函数来创建对象,但是 Object() 构造函数提供了一些非常常用的静态方法,例如 Object.assign() 方法和 Object.create() 方法等。

接下来,让我们深入分析一下 TypeScript 源码中对 Object() 构造函数的类型定义。下面仅摘取一部分着重关注的类型定义:

interface ObjectConstructor {

    readonly prototype: Object;

    // 省略了其他成员
}

declare var Object: ObjectConstructor;

由该定义能够直观地了解到 Object() 构造函数的类型是 ObjectConstructor 类型而不是 Object 类型,它们是不同的类型。第 3 行,prototype 属性的类型为 Object 类型。构造函数的 prototype 属性值决定了实例对象的原型。此外,Object.prototype 是一个特殊的对象,它是 JavaScript 中的公共原型对象。也就是说,如果程序中没有刻意地修改一个对象的原型,那么该对象的原型链上就会有 Object.prototype 对象,因此也会继承 Object.prototype 对象上的属性和方法。

现在,我们可以正式地引出 Object 类型。Object 类型是特殊对象 Object.prototype 的类型,该类型的主要作用是描述 JavaScript 中几乎所有对象都共享(通过原型继承)的属性和方法。Object 类型的具体定义如下所示(取自 TypeScript 源码):

interface Object {
    /**
     * The initial value of Object.prototype.constructor
     * is the standard built-in Object constructor.
     */
    constructor: Function;

    /**
     * Returns a string representation of an object.
     */
    toString(): string;

    /**
     * Returns a date converted to a string using the
     * current locale.
     */
    toLocaleString(): string;

    /**
     * Returns the primitive value of the specified object.
     */
    valueOf(): Object;

    /**
     * Determines whether an object has a property with
     * the specified name.
     * @param v A property name.
     */
    hasOwnProperty(v: PropertyKey): boolean;

    /**
     * Determines whether an object exists in another
     * object's prototype chain.
     */
    isPrototypeOf(v: Object): boolean;

    /**
     * Determines whether a specified property is enumerable.
     * @param v A property name.
     */
    propertyIsEnumerable(v: PropertyKey): boolean;
}

通过该类型定义能够了解到,Object 类型里定义的方法都是通用的对象方法,如 valueOf() 方法。

类型兼容性

Object 类型有一个特点,那就是除了 undefined 值和 null 值外,其他任何值都可以赋值给 Object 类型。示例如下:

let obj: Object;

// 正确
obj = { x: 0 };
obj = true;
obj = 'hi';
obj = 1;

// 编译错误
obj = undefined;
obj = null;

对象能够赋值给 Object 类型是理所当然的,但为什么原始值也同样能够赋值给 Object 类型呢?实际上,这样设计正是为了遵循 JavaScript 语言的现有行为。我们在本章开篇处介绍了 JavaScript 语言中存在自动封箱操作。当在原始值上调用某个方法时,JavaScript 会对原始值执行封箱操作,将其转换为对象类型,然后再调用相应方法。Object 类型描述了所有对象共享的属性和方法,而 JavaScript 允许在原始值上直接访问这些方法,因此 TypeScript 允许将原始值赋值给 Object 类型。示例如下:

'str'.valueOf();

const str: Object = 'str';
str.valueOf();

常见错误

在使用 Object 类型时容易出现的一个错误是,将 Object 类型应用于自定义变量、参数或属性等的类型。示例如下:

const point: Object = { x: 0, y: 0 };

此例中,将常量 point 的类型定义为 Object 类型。虽然该代码不会产生任何编译错误,但它是一个明显的使用错误。原因刚刚介绍过,Object 类型的用途是描述 Object.prototype 对象的类型,即所有对象共享的属性和方法。在描述自定义对象类型时有很多更好的选择,完全不需要使用 Object 类型,例如接下来要介绍的 object 类型和对象字面量类型等。在 TypeScript 官方文档中也明确地指出了不应该使用 Object 类型,而是应该使用 object 类型来代替。

object

在 TypeScript 2.2 版本中,增加了一个新的 object 类型表示非原始类型。object 类型使用 object 关键字作为标识,object 类型名中的字母全部为小写。示例如下:

const point: object = { x: 0, y: 0 };

object 类型的关注点在于类型的分类,它强调一个类型是非原始类型,即对象类型。object 类型的关注点不是该对象类型具体包含了哪些属性,例如对象类型是否包含一个名为 name 的属性,因此,不允许读取和修改 object 类型上的自定义属性。示例如下:

const obj: object = { foo: 0 };

// 编译错误!属性'foo'不存在于类型'object'上
obj.foo;

// 编译错误!属性'foo'不存在于类型'object'上
obj.foo = 0;

object 类型上仅允许访问对象的公共属性和方法,也就是 Object 类型中定义的属性和方法。示例如下:

const obj: object = {};

obj.toString();
obj.valueOf();

类型兼容性

我们知道,JavaScript 中的数据类型可以划分为原始数据类型和对象数据类型两大类。针对 JavaScript 中的每一种原始数据类型,TypeScript 都提供了对应的类型:

  • boolean

  • string

  • number

  • bigint

  • symbol

  • undefined

  • null

但是在以前的版本中,TypeScript 唯独没有提供一种类型用来表示非原始类型,也就是对象类型。上一节介绍的 Object 类型无法表示非原始类型,因为允许将原始类型赋值给 Object 类型。例如,将字符串赋值给 Object 类型不会产生错误。示例如下:

const a: Object = 'hi';

新的 object 类型填补了这个功能上的缺失。object 类型能够准确地表示非原始类型,因为原始类型不允许赋给 object 类型。示例如下:

let nonPrimitive: object;

// 下列赋值语句均会产生编译错误
nonPrimitive = true;
nonPrimitive = 'hi';
nonPrimitive = 1;
nonPrimitive = 1n;
nonPrimitive = Symbol();
nonPrimitive = undefined;
nonPrimitive = null;

只有非原始类型,也就是对象类型能够赋给 object 类型。示例如下:

let nonPrimitive: object;

// 正确
nonPrimitive = {};
nonPrimitive = { x: 0 };
nonPrimitive = [0];
nonPrimitive = new Date();
nonPrimitive = function () {};

object 类型仅能够赋值给以下三种类型:

  • 顶端类型 anyunknown

  • Object 类型。

  • 空对象类型字面量 {}(将在 5.11.3 节中介绍)。

由于所有类型都是顶端类型的子类型,所以 object 类型能够赋值给顶端类型 anyunknown。示例如下:

const nonPrimitive: object = {};

const a: any = nonPrimitive;
const b: unknown = nonPrimitive;

Object 类型描述了所有对象都共享的属性和方法,所以很自然地表示对象类型的 object 类型能够赋值给 Object 类型。示例如下:

const nonPrimitive: object = {};

const obj: Object = nonPrimitive;

object 类型也能够赋值给空对象类型字面量 {}。我们将在 5.11.3 节中介绍空对象类型字面量。示例如下:

const nonPrimitive: object = {};

const obj: {} = nonPrimitive;

实例应用

JavaScript 中,有一些内置方法只接受对象作为参数。例如,我们前面提到的 Object.create() 方法,该方法的第一个参数必须传入对象或者 null 值作为新创建对象的原型。如果传入了原始类型的值,例如数字 1,那么将产生运行时的类型错误。示例如下:

// 正确
const a = Object.create(Object.prototype);
const b = Object.create(null);

// 类型错误
const c = Object.create(1);

在没有引入 object 类型之前,没有办法很好地描述 Object.create() 方法签名的类型。TypeScript 也只好将该方法第一个参数的类型定义为 any 类型。如此定义参数类型显然不够准确,而且对类型检查也没有任何帮助。示例如下:

interface ObjectConstructor {
    create(o: any, ...): any;

    // 省略了其他成员
}

在引入了 object 类型之后,TypeScript 更新了 Object.create() 方法签名的类型,使用 object 类型来替换 any 类型。示例如下:

interface ObjectConstructor {
    create(o: object | null, ...): any;

    // 省略了其他成员
}

现在,我们能够正确描述 Object.create() 方法的参数类型。如果传入了原始类型的参数,编译器在进行静态类型检查时就能够发现这个错误。示例如下:

const a = Object.create(1);
//                      ~
//                      编译错误

对象类型字面量

对象类型字面量是定义对象类型的方法之一。下例中,我们使用对象类型字面量定义了一个对象类型。该对象类型中包含了两个属性成员 xy,它们的类型均为 number 类型。示例如下:

const point: { x: number; y: number } = { x: 0, y: 0 };
//             ~~~~~~~~~~~~~~~~~~~~~~~~
//             对象类型字面量

接下来,将介绍对象类型字面量的具体使用方法。

基础语法

对象类型字面量的语法与对象字面量的语法相似。在定义对象类型字面量时,需要将类型成员依次列出。对象类型字面量的语法如下所示:

{
    TypeMember;
    TypeMember;
    ...
}

在该语法中,TypeMember 表示对象类型字面量中的类型成员,类型成员置于一对大括号 {} 之内。在各个类型成员之间,不但可以使用分号 ; 进行分隔,还可以使用逗号 , 进行分隔,这两种分隔符不存在功能上的差异。示例如下:

{
    TypeMember,
    TypeMember,
    ...
}

类型成员列表中的尾后分号和尾后逗号是可选的。示例如下:

{
    TypeMember;
    TypeMember;
}

{
    TypeMember;
    TypeMember
}

对象类型字面量的类型成员可分为以下五类:

  • 属性签名

  • 调用签名

  • 构造签名

  • 方法签名

  • 索引签名

下面我们将以属性签名为例来介绍对象类型字面量的使用方法,其他种类的类型成员将在 5.12 节和 5.13 节中进行详细介绍。

属性签名

属性签名声明了对象类型中属性成员的名称和类型。它的语法如下所示:

{
    PropertyName: Type;
}

在该语法中,PropertyName 表示对象属性名,可以为标识符、字符串、数字和可计算属性名;Type 表示该属性的类型。

下例中,我们使用对象类型字面量定义了 Point 对象类型,该类型表示二维坐标系中的点。Point 对象类型包含两个属性签名类型成员,分别为表示横坐标的属性 x 和表示纵坐标的属性 y,两者的类型均为 number 类型。示例如下:

let point: { x: number; y: number } = { x: 0, y: 0 };

属性签名中的属性名可以为可计算属性名,但需要该可计算属性名满足以下条件之一:

  • 可计算属性名的类型为 string 字面量类型或 number 字面量类型。示例如下:

    const a: 'a' = 'a';
    const b: 0 = 0;
    
    let obj: {
        [a]: boolean;
        [b]: boolean;
    
        ['c']: boolean;
        [1]: boolean;
    };
  • 可计算属性名的类型为 unique symbol 类型。示例如下:

    const s: unique symbol = Symbol();
    
    let obj: {
        [s]: boolean;
    };
  • 可计算属性名符合 Symbol.xxx 的形式。示例如下:

    let obj: {
        [Symbol.toStringTag]: string;
    };

在属性签名的语法中,表示类型的 Type 部分是可以省略的,允许只列出属性名而不定义任何类型。在这种情况下,该属性的类型默认为 any 类型。示例如下:

{
    x;
    y;
}

// 等同于:

{
    x: any;
    y: any;
}

注意,此例中的代码仅在没有启用 --noImplicitAny 编译选项的情况下才能够正常编译。若启用了 --noImplicitAny 编译选项,则会产生编译错误,因为对象属性隐式地获得了 any 类型。示例如下:

{
    x;
//  ~
//  编译错误!成员 'x' 隐式地获得了 'any' 类型
}

在程序中,不推荐省略属性签名中的类型。

可选属性

在默认情况下,通过属性签名定义的对象属性是必选属性。如果在属性签名中的属性名之后添加一个问号 ?,那么将定义一个可选属性。定义可选属性成员的语法如下所示:

{
    PropertyName?: Type;
}

在给对象类型赋值时,可选属性可以被忽略。下例中,我们修改了前面定义的 Point 对象类型,添加一个可选属性 z 来表示点的 Z 轴坐标。这样 Point 对象类型也能够表示三维坐标系中的点。示例如下:

let point: { x: number; y: number; z?: number };
//           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//           Point对象类型

point = { x: 0, y: 0 };
point = { x: 0, y: 0, z: 0 };

此例中,Point 对象类型的属性 z 是可选属性。在给 point 变量赋值时,既可以为属性 z 赋予一个 number 类型的值,也可以完全忽略属性 z

--strictNullChecks 模式下,TypeScript 会自动在可选属性的类型定义中添加 undefined 类型。因此,下例中两个 Point 对象类型的定义是等价的:

{
    x: number;
    y: number;
    z?: number;
};

// 等同于:
{
    x: number;
    y: number;
    z?: number | undefined;
};

该行为的结果是,我们可以为可选属性传入 undefined 值来明确地表示忽略该属性的值,示例如下:

let point: { x: number; y: number; z?: number };

point = { x: 0, y: 0 };
point = { x: 0, y: 0, z: undefined };
point = { x: 0, y: 0, z: 0 };

同时也要注意,在 --strictNullChecks 模式下,null 类型与 undefined 类型是区别对待的。此例中,不允许给属性 z 赋予 null 值,如下所示:

let point: { x: number; y: number; z?: number };

point = {
    x: 0,
    y: 0,
    z: null,
//  ~
//  编译错误!类型'null'不能赋值给类型'number | undefined'
};

在非 --strictNullChecks 模式下,null 值与 undefined 值均可以赋值给可选属性。因为在该模式下,null 值与 undefined 值几乎可以赋值给任意类型。

在操作对象类型的值时,只允许读写对象类型中已经定义的必选属性和可选属性。若访问了未定义的属性,则会产生编译错误。例如,下例中 point 的类型里没有定义属性 t,因此不允许读写属性 t

let point: { x: number; y: number; z?: number };

// 正确
point = { x: 0, y: 0 };
point.x;
point.y;

// 正确
point = { x: 0, y: 0, z: 0 };
point.x;
point.y;
point.z;

point = { x: 0, y: 0, z: 0, t: 0 }; // 编译错误
point.t;                            // 编译错误

只读属性

在属性签名定义中添加 readonly 修饰符能够定义对象只读属性。定义只读属性的语法如下所示:

{
    readonly PropertyName: Type;
}

下例中,我们将 Point 对象类型中的属性 x 和属性 y 定义为只读属性:

let point: {
    readonly x: number;
    readonly y: number;
};

point = { x: 0, y: 0 };

只读属性的值在初始化后不允许再被修改,示例如下:

let point: {
    readonly x: number;
    readonly y: number;
};

// 正确,初始化
point = { x: 0, y: 0 };

point.x = 1;
//    ~
//    编译错误!不允许给x赋值,因为它是只读属性

point.y = 1;
//    ~
//    编译错误!不允许给y赋值,因为它是只读属性

空对象类型字面量

如果对象类型字面量没有定义任何类型成员,那么它就成了一种特殊的类型,即空对象类型字面量 {}。空对象类型字面量表示不带有任何属性的对象类型,因此不允许在 {} 类型上访问任何自定义属性。示例如下:

const point: {} = { x: 0, y: 0 };

point.x;
//    ~
//    编译错误!属性 'x' 不存在于类型 '{}'

point.y;
//    ~
//    编译错误!属性 'y' 不存在于类型 '{}'

在空对象类型字面量 {} 上,允许访问对象公共的属性和方法,也就是 Object 类型上定义的方法和属性。示例如下:

const point: {} = { x: 0, y: 0 };

point.valueOf();

现在,读者可能会发现空对象类型字面量 {}Object 类型十分相似。而事实上也正是如此,单从行为上来看两者是可以互换使用的。例如,除了 undefined 值和 null 值外,其他任何值都可以赋值给空对象类型字面量 {} 和 Object 类型。同时,空对象类型字面量 {}Object 类型之间也允许互相赋值。示例如下:

let a: Object = 'hi';
let b: {} = 'hi';

a = b;
b = a;

两者的区别主要在于语义上。全局的 Object 类型用于描述对象公共的属性和方法,它相当于一种专用类型,因此程序中不应该将自定义变量、参数等类型直接声明为 Object 类型。空对象类型字面量 {} 强调的是不包含属性的对象类型,同时也可以作为 Object 类型的代理来使用。最后,也要注意在某些场景中新的 object 类型可能是更加合适的选择。

弱类型

弱类型(Weak Type)是 TypeScript 2.4 版本中引入的一个概念。弱类型指的是同时满足以下条件的对象类型:

  • 对象类型中至少包含一个属性。

  • 对象类型中所有属性都是可选属性。

  • 对象类型中不包含字符串索引签名、数值索引签名、调用签名和构造签名(详细介绍请参考5.13节)。

例如,下例中 config 变量的类型是一个弱类型:

let config: {
    url?: string;
    async?: boolean;
    timeout?: number;
};

多余属性

对象多余属性可简单理解为多出来的属性。多余属性会对类型间关系的判定产生影响。例如,一个类型是否为另一个类型的子类型或父类型,以及一个类型是否能够赋值给另一个类型。显然,多余属性是一个相对的概念,只有在比较两个对象类型的关系时谈论多余属性才有意义。

假设存在源对象类型和目标对象类型两个对象类型,那么当满足以下条件时,我们说源对象类型相对于目标对象类型存在多余属性,具体条件如下:

  • 源对象类型是一个 全新(Fresh)的对象字面量类型。

  • 源对象类型中存在一个或多个在目标对象类型中不存在的属性。全新的对象字面量类型 指的是由对象字面量推断出的类型,如图5-1所示。

image 2024 02 07 00 13 02 099
Figure 1. 图5-1 全新的对象字面量类型

此例中,由赋值语句右侧的对象字面量 { x: 0, y: 0 } 推断出的类型为全新的对象字面量类型 { x: 0, y: 0 }。同时也要注意区分,赋值语句左侧类型注解中的 {x: number, y: number } 不是全新的对象字面量类型。如果我们将赋值语句右侧的类型视作源对象类型,将赋值语句左侧的类型视作目标对象类型,那么不存在多余属性。

我们对这段代码稍加修改,如下所示:

const point: { x: number; y: number } = {
    x: 0,
    y: 0,
    z: 0,
//  ~~~~
//  z是多余属性
};

我们为赋值语句右侧的对象字面量增加了一个 z 属性。这时,赋值语句右侧的类型仍为全新的对象字面量类型。若仍将 { x: number, y: number } 视为目标对象类型,那么源对象类型 { x: 0, y: 0, z: 0 } 存在一个多余属性 z

目标对象类型中的可选属性与必选属性是被同等对待的。例如,下例中 point 的类型为弱类型,而赋值语句右侧源类型中的属性 z 仍然是多余属性:

const point: { x?: number; y?: number } = {
    x: 0,
    y: 0,
    z: 0,
//  ~~~~
//  z是多余属性
};

多余属性检查

多余属性检查是 TypeScript 1.6 引入的功能。多余属性会影响类型间的子类型兼容性以及赋值兼容性,也就是说编译器不允许在一些操作中存在多余属性。例如,将对象字面量赋值给变量或属性时,或者将对象字面量作为函数参数来调用函数时,编译器会严格检查是否存在多余属性。若存在多余属性,则会产生编译错误。示例如下:

let point: {
    x: number;
    y: number;
} = { x: 0, y: 0, z: 0 }; // 4
//                ~~~~
//                编译错误!z是多余属性

function f(point: { x: number; y: number }) {}
f({ x: 0, y: 0, z: 0 }); // 9
//              ~~~~
//              编译错误!z是多余属性

此例第 4 行的赋值语句中,属性 z 是多余属性,因此编译器不允许该赋值操作并产生编译错误。同理,在第 9 行的函数调用语句中,属性 z 是多余属性,编译器也会产生编译错误。

在了解了多余属性检查的基本原理之后,让我们来思考一下它背后的设计意图。在正常的使用场景中,如果我们直接将一个对象字面量赋值给某个确定类型的变量,那么通常没有理由去故意添加多余属性。考虑如下代码:

const point: { x: number; y: number } = {
    x: 0,
    y: 0,
    z: 0,
//  ~~~~
//  z是多余属性
};

此例中明确定义了常量 point 的类型是只包含两个属性x和y的对象类型。在使用对象字面量构造该类型的值时,自然而然的做法是构造一个完全符合该类型定义的值,即只包含两个属性 xy 的对象,完全没有理由再添加多余的属性。

我们再看一个函数调用的场景,如下所示:

function f(point: { x: number; y: number }) {
    point;
}

f({ x: 0, y: 0, z: 0 }); // 5
//              ~~~~
//              z是多余属性

此例中,函数参数 point 的类型为 { x: number; y: number }。第 5 行,调用函数 f 时传入的对象字面量带有多余属性 z,这很可能是一个误操作。

让我们再换一个角度,从类型可靠性的角度来看待多余属性检查。当把对象字面量赋值给目标对象类型时,若存在多余属性,那么将意味着对象字面量本身的类型彻底丢失了,如图5-2所示。

image 2024 02 07 11 26 59 131
Figure 2. 图5-2 多余属性检查

此例中,将包含多余属性的对象字面量赋值给类型为 { x: number; y: number }point 常量后,程序中就再也无法引用对象字面量 { x: 0, y: 0, z: 0 } 的类型了。从类型系统的角度来看,该赋值操作造成了类型信息的永久性丢失,因此编译器认为这是一个错误。

多余属性检查能够带来的最直接的帮助是发现属性名的拼写错误。示例如下:

const task: { canceled?: boolean } = { cancelled: true };
//                                     ~~~~~~~~~~~~~~~
//                                     编译错误!对象字面量只允许包含已知属性
//                                     'cancelled'不存在于'{ canceled?: boolean }' 类型中
//                                     是否指的是'canceled'属性

此例中,常量 task 的类型为 { canceled?: boolean }。其中,canceled 属性是可选属性,因此允许不设置该属性的值。在赋值语句右侧的 { cancelled: true } 对象字面量中,只包含一个 cancelled 属性。仔细查看该代码会发现,对象字面量 { cancelled: true }{ canceled?:boolean } 类型中的属性名拼写相差了一个字母 l。如果编译器不进行多余属性检查,那么此例中的代码不会产生编译错误。更糟糕的是,常量 task 中的 canceled 属性没有按照预期被设置为 true,而是使用默认值 undefinedundefined 是一个 假 值,它与想要设置的 true 正好相反。这就给程序注入了一个让人难以察觉的错误。

如果编译器能够执行多余属性检查,那么它能够识别出对象字面量中的 cancelled 属性是一个多余属性,从而产生编译错误。更好的是,编译器不但能够提示多余属性的错误,还能够根据 Levenshtein distance 算法来推测可能的属性名。这也是为什么在第 5 行中,编译器能够提示出 是否指的是 canceled 属性? 这条消息。

允许多余属性

在前文中,我们介绍了什么是多余属性以及为什么要进行多余属性检查。多余属性检查在绝大多数场景中都是合理的,因此推荐在程序中尽可能地利用这个功能。但如果确定不想让编译器对代码进行多余属性检查,那么有多种方式能够实现这个效果。接下来,让我们以如下的代码为例来介绍每一种方法:

const point: { x: number } = { x: 0, y: 0 };
//                                   ~~~~
//                                   y是多余属性

能够忽略多余属性检查的方法如下:

  • 使用类型断言,这是推荐的方法。

    类型断言能够对类型进行强制转换。例如,我们可以将对象字面量 { x: 0, y: 0 } 的类型强制转换为 { x: number } 类型。关于类型断言的详细介绍请参考6.10节。类型断言能够绕过多余属性检查的真正原因是,处于类型断言表达式中的对象字面量将不再是 全新的对象字面量类型,因此编译器也就不会对其进行多余属性检查,下例中的第 5 行代码能够证明这一点:

    // 无编译错误
    const p0: { x: number } = { x: 0, y: 0 } as { x: number };
    
    // 无编译错误
    const p1: { x: number } = { x: 0, y: 0 } as { x: 0; y: 0 };
  • 启用 --suppressExcessPropertyErrors 编译选项。启用该编译选项能够完全禁用整个 TypeScript 工程的多余属性检查,但同时也将完全失去多余属性检查带来的帮助。我们可以在 tsconfig.json 配置文件中或命令行上启用该编译选项。关于配置文件的详细介绍请参考8.3节。示例如下:

    {
        "compilerOptions": {
            "suppressExcessPropertyErrors": true
        }
    }
  • 使用 // @ts-ignore 注释指令。

    该注释指令能够禁用针对某一行代码的类型检查。关于注释指令的详细介绍请参考8.5.2节。示例如下:

    // @ts-ignore
    const point: { x: number } = { x: 0, y: 0 };
  • 为目标对象类型添加索引签名。

    若目标对象类型上存在索引签名,那么目标对象可以接受任意属性,因此也就谈不上多余属性。关于索引签名的详细介绍请参考5.13.6节。示例如下:

    const point: {
        x: number;
        [prop: string]: number; // 索引签名
    } = { x: 0, y: 0 };
  • 最后一种方法也许不是很好理解。如果我们先将对象字面量赋值给某个变量,然后再将该变量赋值给目标对象类型,那么将不会执行多余属性检查。这种方法能够生效的原理与类型断言类似,那就是令源对象类型不为 全新的对象字面量类型,于是编译器将不执行多余属性检查。下面代码的第 4 行,赋值语句右侧不是对象字面量,而是一个标识符,因此 temp 的类型不是 全新的对象字面量类型:

const temp = { x: 0, y: 0 };

// 无编译错误
const point: { x: number } = temp;