TypeScript
的核心原则之一是对值所具备的结构进行类型检查。 它有时被称作“鸭式辨型法(会走路、游泳和呱呱叫的鸟就是鸭子)”或“结构性子类型化”。 在TypeScript
里,接口的做用就是为这些类型命名和为你的代码或第三方代码定义契约。html
在官方教程中有这样一个例子:git
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),但这只意味着接口实例里能够没有这个的属性,并不意味着能够多出其余的属性。检查是否有不在接口定义中的属性,就是额外的属性检查。github
官方教程中的color和colour实在是容易误导人,因此咱们换一个opacity这个属性。而后就获得了一个报错)typescript
而后灵异的事情发生了,我分明记得教程中第一个例子里,接口只定义了一个属性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)
这样函数中的一个参数,也不会触发额外属性检查,由于他会走另外一个逻辑:类型兼容性spa
回到上面的例子,在定义myObj
的时候,并无指定它的类型,因此TS会推断他的类型为{ size: number; label: string; }
。当他做为参数传入printLabel
函数时,ts会比较它和LabelledValue
是否兼容,由于LabelledValue
中的label属性的,myObj
也存在,因此他们是兼容的,这就是最上面提到的鸭式辨型法。3d
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核心源码竟然不开源,果真很微软):code
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点:
用一个例子来讲明
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,两个都是新鲜的对象字面量,没有发生类型推断,因此新鲜度没有消失,会触发额外属性检查。