类型缩窄的工作原理
控制流分析(Control Flow Analysis)
TypeScript 的类型缩窄基于控制流分析——编译器追踪代码的每一条可能执行路径,在每个分支上计算变量的精确类型。这不是简单的语法检查,而是一种数据流分析算法,能处理复杂的条件嵌套、提前返回等场景。
is 关键字声明返回类型谓词。param is Type 形式。当函数返回 true 时,TypeScript 将 param 的类型缩窄为 Type。用于将复杂的运行时类型检查逻辑封装为可复用的工具函数。注意:TypeScript 相信你的谓词函数实现是正确的,如果谓词函数写错,类型系统无法发现。类型缩窄(Type Narrowing)
什么是类型缩窄?
TypeScript 的类型缩窄是指:在代码的特定分支中,TypeScript 能根据条件判断,将一个宽泛类型(如 string | number)自动缩窄为更精确的类型。这让你可以安全地使用该分支中已确定的类型特有方法。
typeof 类型保护
function process(value: string | number | boolean) {
if (typeof value === 'string') {
// 此块中 value: string
console.log(value.toUpperCase());
} else if (typeof value === 'number') {
// 此块中 value: number
console.log(value.toFixed(2));
} else {
// 此块中 value: boolean(已排除其他可能)
console.log(value ? '是' : '否');
}
}
instanceof 类型保护
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
function handleError(error: unknown) {
if (error instanceof ApiError) {
// error: ApiError
console.log(`HTTP ${error.statusCode}: ${error.message}`);
} else if (error instanceof Error) {
// error: Error
console.log(`Error: ${error.message}`);
} else {
console.log('Unknown error', error);
}
}
自定义类型谓词(Type Predicates)
interface Fish { swim: () => void; name: string; }
interface Bird { fly: () => void; name: string; }
// is 关键字:返回值类型是类型谓词
function isFish(animal: Fish | Bird): animal is Fish {
return ('swim' in animal);
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim(); // animal: Fish
} else {
animal.fly(); // animal: Bird
}
}
可辨识联合(Discriminated Union)
最强大的类型缩窄模式
// 每个联合成员有一个「标签字段」(discriminant)
type Result<T> =
| { kind: 'success'; data: T; }
| { kind: 'error'; code: number; message: string; }
| { kind: 'loading'; progress: number; };
function handleResult<T>(result: Result<T>) {
switch (result.kind) {
case 'success':
console.log(result.data); // data: T
break;
case 'error':
console.error(`[${result.code}] ${result.message}`);
break;
case 'loading':
console.log(`加载中... ${result.progress}%`);
break;
}
}
in 运算符与属性缩窄
// in 运算符:检查属性是否存在
interface Dog { bark: () => void; breed: string; }
interface Cat { purr: () => void; indoor: boolean; }
function speak(animal: Dog | Cat) {
if ('bark' in animal) {
animal.bark(); // animal: Dog
} else {
animal.purr(); // animal: Cat
}
}
// 真值缩窄:过滤 null/undefined
function printLength(s: string | null | undefined) {
if (s) {
// s: string(null 和 undefined 都是假值)
console.log(s.length);
}
}
// 注意:空字符串 "" 是假值!下面是更精确的缩窄
function printLength2(s: string | null | undefined) {
if (s != null) { // != null 同时排除 null 和 undefined
console.log(s.length); // s: string(包括空字符串)
}
}
使用 if (value) 缩窄 string | null 时,空字符串 "" 也会被过滤掉,因为空字符串是假值。如果业务中空字符串和 null 应有不同处理,必须用 value !== null && value !== undefined 或 value != null(== 宽松比较)。同理,数字 0 也是假值,不能用 if (count) 来判断"count 存在"。
satisfies 运算符(TypeScript 4.9)
type Config = {
[key: string]: string | number | boolean;
};
// 问题:as 断言丢失字面量类型
const config1 = {
host: 'localhost', // 推断为 string
port: 3000, // 推断为 number
} as Config;
// satisfies:保留字面量类型的同时验证符合 Config
const config2 = {
host: 'localhost', // 仍然是 'localhost' 字面量类型
port: 3000, // 仍然是 3000 字面量类型
} satisfies Config;
// 因为保留了具体类型,可以直接调用字符串方法
config2.host.toUpperCase(); // ✅(config1 不行,因为 string|number|boolean 没有这个方法)
TypeScript 的类型缩窄让你可以写出既安全又表达力强的代码。可辨识联合(带 kind/type/tag 字段的联合类型)是最推荐的模式——它让 switch/if 语句成为类型安全的穷举检查,比 class 继承更灵活,比简单的 if-else 更健壮。Redux 的 action、React 的状态机、API 响应类型都大量使用这一模式。
穷举检查(Exhaustiveness Checking)
可辨识联合配合 never 类型,可以让 TypeScript 强制检查是否所有 case 都被处理:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function assertNever(x: never): never {
throw new Error(`未处理的形状: ${JSON.stringify(x)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return 0.5 * shape.base * shape.height;
default:
// 如果 Shape 类型新增了一个成员但这里没处理,
// assertNever 会让 TypeScript 编译报错
return assertNever(shape);
}
}
类型断言的正确使用
// as 断言:告诉 TypeScript "我比你更了解这个类型"
const input = document.getElementById('search') as HTMLInputElement;
input.value = 'hello'; // 可以访问 HTMLInputElement 的属性
// ! 非空断言:断言值不为 null/undefined
const canvas = document.getElementById('canvas')!; // 断言不为 null
// 双重断言(慎用):任何类型 as unknown as 目标类型
// 用于临时绕过类型检查,正式代码中应避免
const hack = someValue as unknown as string;
// as const:将值的类型收窄为字面量类型
const STATUS = {
ACTIVE: 'active',
INACTIVE: 'inactive',
} as const;
// STATUS.ACTIVE 的类型是 'active'(字面量),而不是 string
type Status = typeof STATUS[keyof typeof STATUS];
// Status = 'active' | 'inactive'
控制流分析的边界情况
赋值后类型重置
TypeScript 的控制流分析会追踪每次赋值操作。一旦变量被重新赋值,之前缩窄的类型会被重置为赋值表达式的类型——即使重新赋值发生在 if 分支之后。
function reassignTest(x: string | number) {
if (typeof x === 'string') {
// 此处 x: string
x = x.toUpperCase(); // ✅ 合法
// 重新赋值后 x 的类型变为赋值表达式的类型
}
// 此处 x: string | number(恢复到声明类型)
}
// 闭包会捕获变量的"声明时类型",而不是"缩窄后类型"
function closureTest(x: string | null) {
if (x !== null) {
// 此处 x: string
const fn = () => x.toUpperCase(); // ❌ 错误!
// 闭包中 x 可能在 fn 调用时被重新赋值为 null
// TypeScript 4.4+ 对闭包保守处理:闭包内类型退回到声明类型
}
}
在缩窄块内部创建闭包(箭头函数、回调函数)时,TypeScript 不会将缩窄后的类型传入闭包——因为闭包可能在稍后调用,此时变量的值可能已经改变。解决方案:在闭包外将缩窄后的值赋给一个新的 const 变量:const s = x;,然后闭包捕获 s(类型始终是 string)。
never 类型的传播
throw 语句之后,TypeScript 知道后续代码不可达,并相应地缩窄联合类型。type NonNullable<T> = T extends null | undefined ? never : T。当 T 为联合类型时,never 成员会自动从结果联合中移除。// throw 语句后的类型缩窄
function getUser(id: string | undefined): string {
if (!id) {
throw new Error('id is required');
// throw 之后,id 的类型在后续代码中变为 string(never 被消除)
}
// 此处 id: string(undefined 已在上面 throw 掉)
return id.toUpperCase(); // ✅ 安全
}
// never 过滤联合类型
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null | undefined>;
// A = string(null 和 undefined 映射到 never,从联合中消失)
本章小结
- 类型缩窄的触发条件:typeof/instanceof/in 检查、真值检查、相等检查、类型谓词(is 关键字)都会缩窄联合类型。
- 可辨识联合:每个成员有一个字面量类型的公共字段(discriminant);switch(x.kind) 可以精确缩窄到每个成员,是处理状态机和 API 响应的最佳模式。
- 穷举检查:在 switch 的 default 分支调用 assertNever(x: never),如果新增联合成员但忘记处理,TypeScript 会在编译时报错。
- satisfies 运算符(v4.9+):在验证对象符合某类型的同时,保留属性的具体字面量类型,比 as 断言更安全。
- as const:将对象/数组所有属性收窄为字面量类型,配合 typeof + keyof 可以从常量对象自动生成联合类型。