TypeScript: TypeScript 类型守卫与收窄
最后更新:2026-08-26
类型收窄是 TypeScript 最实用的特性之一——把宽泛的类型缩小到更精确的范围,安全地访问特定类型的属性和方法。
1. 类型收窄概述
联合类型的变量只能访问所有成员共有的属性。类型收窄让我们在特定代码分支中"收窄"类型,访问该分支独有的成员:
TYPESCRIPT
function process(value: string | number) {
// 收窄前——value 只能用共有方法
console.log(value.toString()); // ✅ string 和 number 都有
// 收窄后——value 可以用特定方法
if (typeof value === "string") {
console.log(value.toUpperCase()); // ✅ string 独有方法
} else {
console.log(value.toFixed(2)); // ✅ number 独有方法
}
}
TypeScript 支持以下收窄方式:
| 收窄方式 | 适用场景 |
|---|---|
typeof |
基本类型判断 |
instanceof |
类实例判断 |
in 操作符 |
属性是否存在 |
| 相等性检查 | ===/!== 比较 |
| 自定义类型守卫 | 复杂类型判断 |
| 可赋值收窄 | 赋值时自动收窄 |
2. typeof 收窄
typeof 最常用于区分基本类型:
(1) 基本用法
TYPESCRIPT
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
return " ".repeat(padding) + value; // padding 收窄为 number
}
return padding + value; // padding 收窄为 string
}
console.log(padLeft("hello", 4)); // " hello"
console.log(padLeft("hello", ">>>")); // ">>>hello"
(2) typeof 的返回值
TYPESCRIPT
typeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 100n // "bigint"
typeof {} // "object" ⚠️ 包括 null、数组、Date 等
typeof function(){} // "function"
🔥 易错:
typeof null === "object" 是 JavaScript 的历史 bug。用 typeof 无法区分 null 和其他对象——需要用 === null 判断。
(3) typeof 与 switch
TYPESCRIPT
function describe(value: string | number | boolean | undefined) {
switch (typeof value) {
case "string":
return `字符串:${value.toUpperCase()}`;
case "number":
return `数字:${value.toFixed(2)}`;
case "boolean":
return `布尔:${value}`;
case "undefined":
return "未定义";
}
}
3. instanceof 收窄
instanceof 检查对象是否是某个类的实例——适合引用类型的收窄:
(1) 基本用法
TYPESCRIPT
function formatValue(value: Date | string | Error): string {
if (value instanceof Date) {
return value.toISOString(); // Date 方法
} else if (value instanceof Error) {
return value.message; // Error 属性
} else {
return value.toUpperCase(); // string 方法
}
}
console.log(formatValue(new Date())); // "2024-..."
console.log(formatValue(new Error("出错"))); // "出错"
console.log(formatValue("hello")); // "HELLO"
(2) instanceof 的限制
instanceof 只能用于类实例——不能用于 interface 或 type 别名(它们在运行时不存在):
TYPESCRIPT
interface Dog { bark(): void; }
interface Cat { meow(): void; }
// ❌ instanceof 不能用于接口
// if (pet instanceof Dog) { ... }
// ✅ 需要用自定义类型守卫或 in 操作符
(3) 自定义类与 instanceof
TYPESCRIPT
class NetworkError extends Error {
constructor(public statusCode: number) {
super(`网络错误:${statusCode}`);
}
}
class ValidationError extends Error {
constructor(public field: string) {
super(`验证错误:${field}`);
}
}
function handleError(error: NetworkError | ValidationError): string {
if (error instanceof NetworkError) {
return `HTTP ${error.statusCode} 错误`;
} else {
return `字段 ${error.field} 无效`;
}
}
4. in 操作符收窄
in 检查对象是否有某个属性——适合区分接口/类型别名:
(1) 基本用法
TYPESCRIPT
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // ✅ Fish 类型
} else {
animal.fly(); // ✅ Bird 类型
}
}
(2) 可辨识属性
TYPESCRIPT
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2; // ✅ Circle 的 radius
} else {
return shape.sideLength ** 2; // ✅ Square 的 sideLength
}
}
▶ 示例:类型安全的事件处理
TYPESCRIPT
interface ClickEvent {
type: "click";
x: number;
y: number;
}
interface KeyEvent {
type: "keydown" | "keyup";
key: string;
ctrlKey: boolean;
}
interface ScrollEvent {
type: "scroll";
scrollTop: number;
scrollLeft: number;
}
type UIEvent = ClickEvent | KeyEvent | ScrollEvent;
function handleEvent(event: UIEvent): string {
switch (event.type) {
case "click":
return `点击位置:(${event.x}, ${event.y})`;
case "keydown":
case "keyup":
return `按键:${event.key},Ctrl:${event.ctrlKey}`;
case "scroll":
return `滚动:top=${event.scrollTop}, left=${event.scrollLeft}`;
}
}
console.log(handleEvent({ type: "click", x: 100, y: 200 }));
console.log(handleEvent({ type: "keydown", key: "Enter", ctrlKey: false }));
console.log(handleEvent({ type: "scroll", scrollTop: 50, scrollLeft: 0 }));
输出:
TEXT
📖 仅展示
点击位置:(100, 200)
按键:Enter,Ctrl:false
滚动:top=50, left=0
5. 自定义类型守卫
当内置收窄方式不够用,可以写自定义的类型守卫函数:
(1) 类型谓词(Type Predicate)
TYPESCRIPT
interface Dog {
bark(): void;
breed: string;
}
interface Cat {
meow(): void;
color: string;
}
// 类型谓词:返回值是 "参数名 is 类型"
function isDog(animal: Dog | Cat): animal is Dog {
return "bark" in animal;
}
function interact(animal: Dog | Cat) {
if (isDog(animal)) {
animal.bark(); // ✅ 收窄为 Dog
console.log(animal.breed);
} else {
animal.meow(); // ✅ 收窄为 Cat
console.log(animal.color);
}
}
(2) 断言函数(Assertion Function)
断言函数在条件不满足时抛异常——告诉 TypeScript "如果这行执行了,条件一定成立":
TYPESCRIPT
function assertDefined<T>(value: T | undefined | null, message?: string): asserts value is NonNullable<T> {
if (value == null) {
throw new Error(message ?? "值不能为 null 或 undefined");
}
}
function processUser(user: User | undefined) {
assertDefined(user, "用户不存在");
// 此后 user 类型收窄为 User(不含 undefined)
console.log(user.name.toUpperCase()); // ✅ 安全
}
(3) 断言 vs 类型谓词
| 特性 | 类型谓词 x is T |
断言函数 asserts x is T |
|---|---|---|
| 返回值 | boolean | void(不满足时抛异常) |
| 使用方式 | if (isType(x)) |
assertIsType(x) |
| 类型收窄 | 在 if 分支收窄 | 调用后自动收窄 |
| 适合场景 | 检查后分支处理 | 前置条件检查 |
6. 可赋值收窄
赋值操作也会收窄类型:
TYPESCRIPT
let value: string | number;
value = "hello";
console.log(value.toUpperCase()); // ✅ 赋值后收窄为 string
value = 42;
console.log(value.toFixed(2)); // ✅ 赋值后收窄为 number
(1) 控制流分析
TypeScript 跟踪变量的类型在控制流中的变化:
TYPESCRIPT
function example(x: string | number | boolean) {
// x: string | number | boolean
if (typeof x === "string") {
// x: string
console.log(x.toUpperCase());
} else {
// x: number | boolean
if (typeof x === "number") {
// x: number
console.log(x.toFixed(2));
} else {
// x: boolean
console.log(x);
}
}
}
(2) 收窄与重新赋值
TYPESCRIPT
let value: string | number;
value = "hello";
console.log(value.length); // ✅ string
value = 42;
// console.log(value.length); // ❌ number 没有 length
value = true; // ❌ boolean 不在联合类型中
7. 穷尽检查
确保 switch/if 覆盖所有可能的类型——用 never 类型保证完整性:
TYPESCRIPT
type Shape = "circle" | "square" | "triangle";
function getIcon(shape: Shape): string {
switch (shape) {
case "circle": return "○";
case "square": return "□";
case "triangle": return "△";
default: {
// 如果所有 case 都处理了,shape 在这里是 never
const _exhaustive: never = shape;
return _exhaustive;
}
}
}
// 如果以后 Shape 新增了 "hexagon" 但没加 case
// default 分支的 _exhaustive 会报类型错误
// 这提醒你补全遗漏的 case
(1) 更简洁的穷尽检查
TYPESCRIPT
function assertNever(value: never): never {
throw new Error(`未处理的值:${value}`);
}
type Action = "create" | "update" | "delete";
function handleAction(action: Action) {
switch (action) {
case "create": /* ... */ break;
case "update": /* ... */ break;
case "delete": /* ... */ break;
default:
assertNever(action); // 如果遗漏 case,这里会报类型错误
}
}
❓ 常见问题
Q typeof 和 instanceof 有什么区别?
A typeof 检查值的"基本类型"(string/number/boolean/undefined/object/function),返回字符串,适合基本类型判断。instanceof 检查值是否是某个类的实例,适合引用类型判断(Date、Error、自定义类等)。两者互补,不是替代关系。
Q 自定义类型守卫有什么性能影响?
A 没有。类型守卫只在编译时使用——编译后的 JavaScript 就是普通的 if 判断和属性检查。类型谓词(
x is T)和断言函数(asserts x is T)在运行时不存在,零开销。Q 为什么 in 操作符能收窄类型?
A 因为 TypeScript 知道——如果对象有某个属性,那它一定属于包含该属性的接口。
"swim" in animal 为 true 时,animal 一定实现了包含 swim 的接口。这是逻辑推理,不需要运行时类型信息。Q 什么时候需要写自定义类型守卫?
A 当内置收窄方式(typeof/instanceof/in/===)无法区分类型时。最常见的是区分 interface——interface 在运行时不存在,无法用 instanceof 判断,只能用 in 检查可辨识属性或写自定义类型守卫。
📖 小节
- 类型收窄把宽泛类型缩小到精确范围——是联合类型安全使用的关键
- typeof 判断基本类型,instanceof 判断类实例,in 判断属性是否存在
- 自定义类型守卫(类型谓词
x is T)处理内置方式无法区分的复杂类型 - 断言函数(
asserts x is T)做前置检查,不满足时抛异常 - TypeScript 的控制流分析自动跟踪变量类型在分支中的变化
- 穷尽检查用 never 类型确保 switch/if 覆盖所有可能
📝 作业
- 基础题(难度⭐):写一个函数
doubleOrRepeat(value: string | number)—— 如果 value 是 number 就乘2,如果是 string 就重复拼接一次(如 "hi" → "hihi")。用 typeof 收窄。 - 进阶题(难度⭐⭐):定义
Admin(hasPermission 方法)和Guest(requestAccess 方法)两个接口,用可辨识属性role收窄,写一个函数根据 role 调用不同方法。 - 挑战题(难度⭐⭐⭐):写一个自定义类型守卫
isNonNull<T>(value: T | null | undefined): value is NonNullable<T>,然后在filterNonNull<T>(arr: (T | null | undefined)[]): T[]函数中使用它,过滤掉 null/undefined 并返回非空数组。