TypeScript多余属性

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

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

  • 源对象类型是一个全新(Fresh)的对象字面量类型
  • 源对象类型中存在一个或多个在目标对象类型中不存在的属性。

全新的对象字面量类型指的是由对象字面量推断出的类型,如图所示:

TypeScript多余属性

此例中,由赋值语句右侧的对象字面量“{ 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 };
//                ~~~~
//                编译错误!z是多余属性

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

第4行的赋值语句中,属性z是多余属性,因此编译器不允许该赋值操作并产生编译错误。

第9行的函数调用语句中,属性z是多余属性,编译器也会产生编译错误。

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

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

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

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

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

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

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

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

TypeScript多余属性

此例中,将包含多余属性的对象字面量赋值给类型为“{ 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,而是使用默认值undefined。undefined是一个“假”值,它与想要设置的true正好相反。这就给程序注入了一个让人难以察觉的错误。

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

允许多余属性

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

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

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

  • 使用类型断言,这是推荐的方法。类型断言能够对类型进行强制转换。例如,我们可以将对象字面量“{ x: 0, y: 0 }”的类型强制转换为“{ x: number }”类型。类型断言能够绕过多余属性检查的真正原因是,处于类型断言表达式中的对象字面量将不再是全新的对象字面量类型,因此编译器也就不会对其进行多余属性检查,下例中的第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配置文件中或命令行上启用该编译选项。
{
    "compilerOptions": {
        "suppressExcessPropertyErrors": true
    }
}
  • 使用“// @ts-ignore”注释指令。该注释指令能够禁用针对某一行代码的类型检查。
// @ts-ignore
const point: { x: number } = { x: 0, y: 0 };
  • 为目标对象类型添加索引签名。若目标对象类型上存在索引签名,那么目标对象可以接受任意属性,因此也就谈不上多余属性。
const point: {
    x: number;
    [prop: string]: number; // 索引签名
} = { x: 0, y: 0 };
  • 最后一种方法也许不是很好理解。如果我们先将对象字面量赋值给某个变量,然后再将该变量赋值给目标对象类型,那么将不会执行多余属性检查。这种方法能够生效的原理与类型断言类似,那就是令源对象类型不为“全新的对象字面量类型”,于是编译器将不执行多余属性检查。下面代码的第4行,赋值语句右侧不是对象字面量,而是一个标识符,因此temp的类型不是“全新的对象字面量类型”:
const temp = { x: 0, y: 0 };

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

酷客网相关文章:

赞(1)

评论 抢沙发

评论前必须登录!