Effective TypeScript

区分 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; // OK

2. 使用类型断言

使用类型断言 (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等),但实际没有匹配项。
在 GitHub 上编辑

上次更新于