Effective TypeScript

优先使用类型标注而非类型断言

建议使用类型标注来增强类型检查和安全性,仅在必要时使用类型断言。

标注优于断言

TypeScript 提供了两种为变量指定类型的方式:类型标注(Type Annotations)和类型断言(Type Assertions)。尽管两者都能达到相似的目的,即赋予变量所需的类型,但它们的工作机制和提供的安全性存在本质区别。

类型标注 (: Type)

类型标注的作用是告知 TypeScript 变量应该是什么类型,并验证赋给它的值是否符合该类型定义的约束。如果赋值不符合类型要求(例如缺少必需属性),TypeScript 会立即报错。

类型断言 (as Type)

类型断言的作用是告诉 TypeScript 编译器:“请相信我,我比你更了解这个值的类型。”它抑制了类型检查器可能发现的错误。

由于类型断言会静默错误,因此通常应当优先使用类型标注,除非有充分的理由使用断言。

冗余属性检查

倾向于使用类型标注的主要原因在于其提供了额外的安全机制,即冗余属性检查 (Excess Property Checking, EPC)

虽然 TypeScript 的结构化类型系统(Structural Typing)通常允许对象拥有额外的、未在类型中声明的属性(类型是“开放的”),但 EPC 是一项针对对象字面量的特殊检查,旨在捕捉常见的拼写错误或疏忽。

当使用类型标注时,EPC 生效:

interface Person { 
  name: string; 
}

// 1. 标注捕获缺失属性
const alice: Person = {}; 
// ~~~~~ Error: Property 'name' is missing

// 2. 标注捕获冗余属性(EPC 生效)
const charlie: Person = {
  name: 'Charlie',
  occupation: 'TypeScript developer' 
// ~~~~~~~~~ Error: Object literal may only specify known properties
};

而类型断言会绕过 EPC,导致潜在的错误被忽略:

// 使用断言,错误被静默
const bob = {} as Person; // OK (No error, even though 'name' is missing)

箭头函数中的类型标注

在箭头函数中应用类型标注可能较为复杂。如果想对函数体进行检查,但又想利用 TypeScript 对参数的类型推断,可以只对箭头函数的返回类型进行标注:

// 假设 Person 接口已定义,并且我们从字符串数组映射到 Person 数组
const people = ['alice', 'bob', 'jan'].map(
  (name): Person => ({name}) // 标注返回类型,确保函数体返回的值符合 Person
);

何时使用类型断言

类型断言最适合在你确实比 TypeScript 编译器掌握更多上下文信息时使用。这通常发生在代码与外部环境交互,而编译器无法访问完整的运行时模型时(例如浏览器中的 DOM 操作)。

在这种情况下,断言弥补了编译器在静态分析时缺乏的运行时知识:

// TypeScript 无法从 DOM 结构得知 #myButton 是一个按钮元素
document.querySelector('#myButton')?.addEventListener('click', e => {
  // e.currentTarget 的类型是 EventTarget | null
  // 开发者断言它是一个 HTMLButtonElement
  const button = e.currentTarget as HTMLButtonElement; 
  //    ^? const button: HTMLButtonElement
});

非空断言与断言的限制

非空断言(Non-null assertion, !)是一种特殊的类型断言,用于明确告知编译器某个值不会是 nullundefined。它作为后缀使用:

// elNull 的类型是 HTMLElement | null
const el = document.getElementById('foo')!; 
//    ^? const el: HTMLElement

与所有断言一样,非空断言在编译过程中会被擦除,因此必须谨慎使用。

断言的限制

类型断言并非允许在任意两个类型之间转换。基本规则是:只有当类型 A 和类型 B“可比较”(即它们的类型域存在非空交集)时,才能使用断言进行转换。

如果需要将两个不相关的类型(例如 PersonHTMLElement)进行转换,必须使用 unknown 作为中间桥梁,这被称为“双重断言”,以明确告知编译器正在进行可疑操作:

interface Person { name: string; }
// 错误:HTMLElement 和 Person 几乎没有交集
// const el = document.body as Person; 

// 必须使用 unknown 作为中介类型逃逸检查
const el = document.body as unknown as Person; // OK

术语辨析

类型断言有时会被错误地称为“类型转换”(casts)。但这种术语具有误导性,因为在像 C 语言这样的环境中,“转换”可以改变值的运行时行为(例如将整数转为浮点数)。而 TypeScript 中的类型断言是纯类型层面的构造,在编译时被擦除,不会改变值的运行时行为

在 GitHub 上编辑

上次更新于