TypeScript
的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法(会走路、游泳和呱呱叫的鸟就是鸭子)”或“结构性子类型化”。 在TypeScript
里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
在官方教程中有这样一个例子:
interface Square {
color: string,
area: number
}
interface SquareConfig {
color?: string,
width?: number
}
function createSquare(config: SquareConfig): Square {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
createSquare({ color: "black", opacity: 0.5 });
虽然SquareConfig
里的属性都是可选属性(Optional Properties),但这只意味着接口实例里可以没有这个的属性,并不意味着可以多出其他的属性。检查是否有不在接口定义中的属性,就是额外的属性检查。
官方教程中的color和colour实在是容易误导人,所以我们换一个opacity这个属性。然后就得到了一个报错
)
然后灵异的事情发生了,我分明记得教程中第一个例子里,接口只定义了一个属性label
,然后传入了两个属性label
和size
,为啥就不报错呢?
interface LabelledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
仔细分析了一下两段代码的区别,发现报错的第一个例子中,我们传入函数的是一个类似于{ color: "black", opacity: 0.5 }
的对象字面量(object literal),而在第二个不报错的例子中,我们传入的是一个类似于myObj
的变量(variable)。
由此我们可以看到,TypeScript中额外的属性检查只会应用于对象字面量场景,所以,在TS的官方测试用例里面,我们看到的都是objectLiteralExcessProperties.ts。
用变量的情况下,即使他是类似于function printLabel(labeledObj: LabeledValue)
这样函数中的一个参数,也不会触发额外属性检查,因为他会走另一个逻辑:类型兼容性
回到上面的例子,在定义myObj
的时候,并没有指定它的类型,所以TS会推断他的类型为{ size: number; label: string; }
。当他作为参数传入printLabel
函数时,ts会比较它和LabelledValue
是否兼容,因为LabelledValue
中的label属性的,myObj
也存在,所以他们是兼容的,这就是最上面提到的鸭式辨型法。
interface LabelledValue {
label: string;
}
let labeledObj: LabelledValue;
// myObj的推断类型是{size: number; label: string;}
let myObj = {size: 10, label: "Size 10 Object"};
// 兼容,myObj可以赋值给labeledObj
labeledObj = myObj;
说到这里,好像已经把额外属性检查的中那个令人黑人问号的问题给解释清楚了,直到我从一个commit里把额外属性检查的核心函数hasExcessProperties
给扒出来(吐槽一下TS核心源码居然不开源,果然很微软):
function hasExcessProperties(source: FreshObjectLiteralType, target: Type, reportErrors: boolean): boolean {
if (maybeTypeOfKind(target, TypeFlags.Object) && !(getObjectFlags(target) & ObjectFlags.ObjectLiteralPatternWithComputedProperties)) {
const isComparingJsxAttributes = !!(source.flags & TypeFlags.JsxAttributes);
if ((relation === assignableRelation || relation === comparableRelation) && (isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) {
return false;
}
for (const prop of getPropertiesOfObjectType(source)) {
if (!isKnownProperty(target, prop.name, isComparingJsxAttributes)) {
if (reportErrors) {
Debug.assert(!!errorNode);
if (isJsxAttributes(errorNode) || isJsxOpeningLikeElement(errorNode)) {
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(prop), typeToString(target));
}
else {
if (prop.valueDeclaration) {
errorNode = prop.valueDeclaration;
}
reportError(Diagnosics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1,symbolToString(prop), typeToString(target));
}
return true;
}
}
}
}
return false;
}
当我看到参数里的那个FreshObjectLiteralType
,就发现问题并不简单:对象字面量就对象对象字面量,你给我整个fresh是什么鬼???
然后我就去TS的github上一顿操作(官方文档里肯定没有,不要想了),发现TS的作者ahejlsberg是这样描述这个fresh的问题,核心思想就3点:
- 每个对象字面量在初始化的时候都被认为是新鲜(fresh)的
- 当一个新鲜的对象字面量在赋值给一个非空类型的变量,或者作为一个非空类型的参数时,如果这个对象字面量里没有那个非空类型中指定的属性,就会报错
- 在类型断言后,或者对象字面量的类型被拓展后,新鲜度会消失,此时对象字面量就不再新鲜
用一个例子来说明
interface A {
a: number;
b: string;
}
const test = {
a: 10,
b: "foo",
c: "bar"
}
const a: A[] = [test];
const b: A[] = [{
a: 10,
b: "foo",
c: "bar" // ❌ not assignable type error
}];
const c: A[] = [{
a: 10,
b: "foo",
c: "bar"
} as A];
const d: A[] = [test, {
a: 10,
b: "foo",
c: "bar"
}];
const e: A[] = [{
a: 10,
b: "foo",
c: "bar" // ❌ not assignable type error
}, {
a: 10,
b: "foo",
c: "bar"// ❌ not assignable type error
}];
上面这个例子,a和b就是刚刚讨论的变量不进行额外属性检查问题。
c中我们对新鲜的对象字面量进行了断言操作,所以新鲜度消失,不会进行额外属性检查。
d中,因为有test这个变量的存在,而test又因为赋值时进行了类型推断,推断成一个跟A兼容的类型。因此, 在一个字面量数组中,根据最佳通用类型的推断,对象字面量的类型被拓展成了一个跟A兼容的类型,新鲜度也消失了,不会进行额外属性检查,赋值也成功了。
最后一个e,两个都是新鲜的对象字面量,没有发生类型推断,所以新鲜度没有消失,会触发额外属性检查。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。