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,兩個都是新鮮的對象字面量,沒有發生類型推斷,因此新鮮度沒有消失,會觸發額外屬性檢查。