区分 TypeScript 的标准结构化类型检查与冗余属性检查
结构化类型检查:开放与包容
TypeScript 采用结构化类型系统(Structural Typing),也被称为“鸭子类型”(duck typing),以模仿 JavaScript 的运行时行为[^2] [^3]。在结构化检查中,一个值只要具备类型定义中声明的所有必需属性,即使它拥有额外的、未声明的属性,仍被认为是兼容或可赋值的。因此,TypeScript 中的类型本质上是“开放”或“未封闭”(open/not sealed)的[^4] [^5]:
interface Point2D {
x: number;
y: number;
}
// 结构化检查:Point2D类型是开放的
const vec3D = { x: 3, y: 4, z: 1 };
// 赋值给一个变量(非字面量赋值),不会触发EPC,结构化检查通过
const p: Point2D = vec3D; // OK,因为 vec3D 包含了 x 和 y 属性[^1]冗余属性检查的介入
冗余属性检查(EPC)是一个与结构化检查完全独立的流程[^1] [^6]。它的存在是为了弥补结构化类型系统在对象初始化时的盲区。
EPC 旨在捕获开发者输入字面量对象时,因拼写错误或疏忽造成的属性名不匹配问题。这些错误在运行时可能不会导致异常(例如,可选属性被拼错后只会默默地被忽略),但会违背开发者的真实意图[^7]。
典型示例(拼写错误):
interface Options {
title: string;
darkMode?: boolean; // 注意:大写 M
}
function createWindow(options: Options) { /* ... */ }
// 触发 EPC:因为 { title: '...', darkmode: true } 是一个对象字面量
createWindow({
title: 'Spider Solitaire',
darkmode: true // ~~~~~~~ Error: Object literal may only specify known properties,
}); // but 'darkmode' does not exist in type 'Options'. Did you mean 'darkMode'? [^7]绕过冗余属性检查的途径
EPC 并非类型系统的一般规则,它仅适用于“新鲜”的对象字面量(freshly created objects)。因此,有几种方式可以绕过 EPC,但会丧失其提供的安全保障:
1. 使用中间变量
如果将对象字面量赋值给一个没有明确类型标注的中间变量,该变量的类型将通过推断确定,此时它不再被视为“新鲜”的对象字面量。在后续赋值给目标类型时,只会进行标准的结构化检查,而结构化检查允许冗余属性[^8]。
interface Options { darkMode?: boolean; title: string; }
// 步骤 1: 赋值给中间变量,隐式推断类型 { title: string; darkmode: boolean; }
const intermediate = {
title: 'Ski Free',
darkmode: true // OK,此时 TypeScript 接受了该字面量
};
// 步骤 2: 赋值给目标类型,只进行结构化检查。
// intermediate 不是字面量,因此 EPC 被跳过。
const o: Options = intermediate; // OK2. 使用类型断言
使用类型断言 (as Type) 告诉 TypeScript 开发者比类型检查器更清楚对象的类型。断言会立即禁用 EPC,因为它告诉编译器“信任我,我知道这个类型是对的”,从而牺牲了安全性[^9] [^10] [^11]。
interface Options { darkMode?: boolean; title: string; }
const o = { darkmode: true, title: 'MS Hearts' } as Options; // OK,无错误[^10]
// 优先使用类型标注而非断言,正是为了保留 EPC 提供的安全检查[^11] [^12] [^13]。
// const o: Options = { darkmode: true, title: 'MS Hearts' }; // Error (EPC 开启)弱类型检查
与 EPC 相关的还有针对弱类型的检查。弱类型特指接口中所有属性都是可选属性(optional properties)的类型[^14] [^15]。
由于结构化检查对弱类型几乎不设限制(例如,一个空对象 {} 几乎可以赋值给任何只包含可选属性的接口),TypeScript 引入了一项额外的规则:当赋值给弱类型时,赋值对象必须至少有一个属性与弱类型中的属性匹配。
与 EPC 不同,弱类型检查不是针对对象字面量(“新鲜度”)的,而是针对所有赋值操作的,即使通过中间变量赋值也无法绕过,因为它旨在防止因属性完全不匹配导致的错误[^14]。
// 弱类型:所有属性都是可选的
interface LineChartOptions {
logScale?: boolean;
invertedYAxis?: boolean;
}
// 赋值对象包含一个拼写错误的属性
const opts = { logScale: true };
// ~~~~~~~~ Type '{ logScale: boolean; }' has no properties in common
// with type 'LineChartOptions' -> 触发弱类型检查,阻止赋值[^14]
// 检查器期望至少匹配一个属性(logScale、invertedYAxis等),但实际没有匹配项。上次更新于