关于 type-narrowing
讲解 TypeScript 如何通过控制流将宽泛类型缩小为更具体类型的技能,帮助开发者处理联合类型和可空值,并用检查与类型守卫替代不必要的断言。
主要功能
- 01检查 null 和 undefined
- 02使用 typeof 与 instanceof 收窄
- 03处理可辨识联合类型
- 04创建自定义类型守卫
- 05识别常见收窄陷阱
适用场景
- 01解决可能为 undefined 的错误
- 02处理联合类型值
- 03在条件语句中替换类型断言
- 04检查可空的 DOM 元素
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill type-narrowing安装后在 Claude Code 中输入/type-narrowing确认已加载。
详细介绍
理解类型收窄
概述
类型收窄是 TypeScript 根据控制流将宽泛类型细化为更具体类型的过程。
掌握类型收窄,无需使用类型断言即可编写更简洁的代码,并帮助 TypeScript 理解你的逻辑。
何时使用此技能
- 使用
Type | null或Type | undefined - 处理
string | number这样的联合类型 - 处理可辨识联合(标记联合)
- 遇到“可能未定义”错误
- 避免在条件语句中使用类型断言
铁律
NEVER use type assertions when narrowing would work.
没有例外:
- 不能因为“这样更简单”
- 不能因为“我已经检查过了”
- 不能因为“TypeScript 不理解”
检测:“条件语句中的断言”异味
如果你在 if 代码块中使用 as Type,通常可以改用类型收窄。
// ❌ VIOLATION: Using assertion instead of narrowing
function process(value: string | null) {
if (value !== null) {
console.log((value as string).toUpperCase()); // Unnecessary assertion
}
}
// ✅ CORRECT: TypeScript narrows automatically
function process(value: string | null) {
if (value !== null) {
console.log(value.toUpperCase()); // value is string here
// ^? (parameter) value: string
}
}
收窄技巧
1. null/undefined 检查
const el = document.getElementById('foo');
// ^? const el: HTMLElement | null
if (el) {
el.innerHTML = 'Hello';
// ^? const el: HTMLElement
} else {
el
// ^? const el: null
}
2. typeof 类型保护
function padLeft(value: string, padding: string | number) {
if (typeof padding === 'number') {
return ' '.repeat(padding) + value;
// ^? (parameter) padding: number
}
return padding + value;
// ^? (parameter) padding: string
}
3. instanceof 类型保护
function processDate(input: Date | string) {
if (input instanceof Date) {
return input.toISOString();
// ^? (parameter) input: Date
}
return new Date(input).toISOString();
// ^? (parameter) input: string
}
4. 属性检查(in)
interface Bird { fly(): void; }
interface Fish { swim(): void; }
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly();
// ^? (parameter) animal: Bird
} else {
animal.swim();
// ^? (parameter) animal: Fish
}
}
5. 可辨识联合(标记联合)
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
// ^? (parameter) shape: Circle
case 'rectangle':
return shape.width * shape.height;
// ^? (parameter) shape: Rectangle
}
}
6. Array.isArray
function process(input: string | string[]) {
if (Array.isArray(input)) {
return input.join(', ');
// ^? (parameter) input: string[]
}
return input;
// ^? (parameter) input: string
}
7. 提前抛出/返回
function processElement(el: HTMLElement | null) {
if (!el) {
throw new Error('Element not found');
}
// After the throw, el is narrowed
el.innerHTML = 'Hello';
// ^? (parameter) el: HTMLElement
}
用户自定义类型保护
内置收窄方式不够用时:
interface Cat { meow(): void; }
interface Dog { bark(): void; }
// Type predicate: `pet is Cat`
function isCat(pet: Cat | Dog): pet is Cat {
return 'meow' in pet;
}
function speak(pet: Cat | Dog) {
if (isCat(pet)) {
pet.meow();
// ^? (parameter) pet: Cat
} else {
pet.bark();
// ^? (parameter) pet: Dog
}
}
常见的收窄陷阱
typeof null 的结果是 "object"
function process(value: string | object | null) {
if (typeof value === 'object') {
value // Still includes null!
// ^? string | object | null -> object | null
}
}
// Fix: Check null explicitly first
function process(value: string | object | null) {
if (value === null) return;
if (typeof value === 'object') {
value // Now just object
// ^? (parameter) value: object
}
}
假值
function process(x?: number | string | null) {
if (!x) {
x // Includes 0, "", null, undefined!
// ^? string | number | null | undefined
}
}
回调不会保留收窄结果
function processLater(value: { name?: string }) {
if (value.name) {
setTimeout(() => {
console.log(value.name.toUpperCase());
// ~~~~~~~~~ Object is possibly 'undefined'
}, 100);
}
}
// Fix: Capture the narrowed value
function processLater(value: { name?: string }) {
if (value.name) {
const name = value.name; // Capture as const
setTimeout(() => {
console.log(name.toUpperCase()); // OK
}, 100);
}
}
抵御压力的规范
1. “TypeScript 不理解我的检查”
压力:“我检查过了,但 TypeScript 没有收窄类型”
**回应:**调整检查方式,改用 TypeScript 能理解的模式。
**行动:**使用标准收窄模式之一。必要时创建类型保护。
2. “断言更简单”
压力:“我直接用 as Type,不用写 if 语句了”
**回应:**断言不会在运行时进行验证,类型收窄则会。
**行动:**编写检查。这样代码会更安全。
危险信号——停止并重新考虑
- 在 if/switch 代码块中使用
as Type - 类型收窄不起作用(检查所用模式)
- 使用
typeof x === 'object',却没有检查 null - 对可能为 0 或 "" 的值进行假值检查
- 使用
!代替正确的 null 检查
常见的自我辩解(均不成立)
| 借口 | 事实 |
|---|---|
| “我已经检查过了” | 如果 TypeScript 看不到这项检查,就等于没有检查。 |
| “断言更简短” | 代码更短并不总是更好。 |
| “TypeScript 错了” | 调整检查方式,改用 TS 能理解的模式。 |
快速参考
| 你有 | 使用 | 收窄为 |
|---|---|---|
T | null | if (x) 或 if (x !== null) | T |
string | number | typeof x === 'string' | string |
Dog | Cat | x instanceof Dog | Dog |
A | B(带有 kind) | switch (x.kind) | A 或 B |
| 复杂检查 | 用户自定义类型保护 | 你的类型 |
总结
让 TypeScript 通过控制流收窄类型。不要用断言绕过它。
使用标准收窄模式。必要时创建类型保护。在回调执行前捕获值。TypeScript 的类型收窄功能很强大——学会配合使用,而不是绕开它。
参考
基于 Dan Vanderkam 的《Effective TypeScript》,第 22 条:理解类型收窄。
评论
支持 Markdown