第 100 章:类型断言与类型守卫
学习目标
- 掌握类型断言的两种语法
- 理解类型守卫的常见收窄方式
- 学会自定义类型谓词
- 区分断言与守卫的使用边界
一、类型断言(Type Assertion)
类型断言告诉 TS:「我比你更确定这个值的类型,照我说做」。
1.1 两种语法
typescript
// 写法 1:as 语法(推荐)
const value = someValue as string;
// 写法 2:尖括号(不推荐,与 JSX 冲突)
const value = <string>someValue;1.2 常见使用场景
typescript
// 场景 1:DOM 操作
const input = document.querySelector('#username') as HTMLInputElement;
input.value = 'Tom';
// 场景 2:JSON 解析
interface User { id: number; name: string }
const json = '{"id":1,"name":"Tom"}';
const user = JSON.parse(json) as User;
// 场景 3:联合类型收窄
function getLength(value: string | number): number {
if ((value as string).length !== undefined) {
return (value as string).length;
}
return value.toString().length;
}1.3 双重断言(最后的逃生舱)
typescript
// 当两个类型完全无关时,需要双重断言
const value = 'hello' as unknown as number; // ⚠️ 慎用二、类型断言的陷阱
2.1 错误示例
typescript
// ❌ 断言为不兼容的类型
const num = 123 as string; // ❌ Conversion of type 'number' to type 'string' may be a mistake
// ❌ 过度断言导致运行时错误
const value: string | number = 'hello';
const len = (value as string).length; // ✅ 编译通过
// 但如果运行时 value 是 number,这里就会出错2.2 const 断言
typescript
// 让字面量变成"字面量类型"
const x = 'hello'; // 类型: string
const y = 'hello' as const; // 类型: 'hello'
// 数组
const arr = [1, 2, 3] as const; // 类型: readonly [1, 2, 3]
// 对象
const config = {
api: '/api',
method: 'GET'
} as const;
// 类型: { readonly api: '/api'; readonly method: 'GET' }三、类型守卫(Type Guard)
类型守卫是真正安全的类型收窄方式。
3.1 typeof 守卫
typescript
function padLeft(value: string, padding: string | number): string {
if (typeof padding === 'number') {
return ' '.repeat(padding) + value; // ✅ padding 是 number
}
return padding + value; // ✅ padding 是 string
}typeof 可识别的类型:
typescript
typeof x === 'string' // ✅
typeof x === 'number' // ✅
typeof x === 'bigint' // ✅
typeof x === 'boolean' // ✅
typeof x === 'symbol' // ✅
typeof x === 'undefined' // ✅
typeof x === 'object' // ⚠️ null 也是 'object'
typeof x === 'function' // ✅3.2 真值守卫
typescript
function printName(name: string | null | undefined) {
if (name) {
console.log(name.toUpperCase()); // ✅ 收窄为 string
}
}
// 简化:!
// const actualName = name!;3.3 相等守卫
typescript
function compare(x: string | number, y: string | number) {
if (x === y) {
x.toUpperCase(); // ✅ 收窄为 string(因为 === 只能是同类型)
}
}3.4 in 守卫
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
}
}3.5 instanceof 守卫
typescript
function logDate(date: string | Date) {
if (date instanceof Date) {
console.log(date.toISOString()); // ✅ Date
} else {
console.log(date.length); // ✅ string
}
}3.6 赋值守卫
typescript
function process(value: string | number) {
let result: string;
if (typeof value === 'string') {
result = value; // ✅
} else {
result = String(value); // ✅
}
// 这里 result 是 string
}四、类型谓词(Type Predicate)
自定义类型守卫函数,让 TS 知道某个函数能收窄类型。
4.1 语法
typescript
function 函数名(参数: 宽类型): 参数 is 窄类型 {
return boolean;
}4.2 基础示例
typescript
interface Cat { meow(): void; kind: 'cat' }
interface Dog { bark(): void; kind: 'dog' }
function isCat(animal: Cat | Dog): animal is Cat {
return animal.kind === 'cat';
}
function describe(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow(); // ✅ Cat
} else {
animal.bark(); // ✅ Dog
}
}4.3 常见工具类型谓词
typescript
// 非空检查
function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
// 数组检查
function isArray<T>(value: T | T[]): value is T[] {
return Array.isArray(value);
}
// 字符串检查
function isString(value: unknown): value is string {
return typeof value === 'string';
}4.4 复杂谓词
typescript
type ApiResult<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function isSuccess<T>(result: ApiResult<T>): result is { status: 'success'; data: T } {
return result.status === 'success';
}
function handle<T>(result: ApiResult<T>) {
if (isSuccess(result)) {
console.log(result.data); // ✅ T
} else {
console.error(result.error); // ✅ Error
}
}五、断言守卫(Assertion Function)
断言函数抛出错误或改变类型。
5.1 语法
typescript
function 函数名(参数): asserts 参数 is 类型 {
// 抛错或不做任何事
}5.2 基础示例
typescript
function assertString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('必须是字符串');
}
}
function process(value: unknown) {
assertString(value);
value.toUpperCase(); // ✅ 断言后是 string
}5.3 应用:API 参数校验
typescript
function assertUser(obj: unknown): asserts obj is User {
if (!obj || typeof obj !== 'object') {
throw new Error('不是对象');
}
const u = obj as User;
if (typeof u.id !== 'number' || typeof u.name !== 'string') {
throw new Error('字段类型错误');
}
}
function handle(apiResponse: unknown) {
assertUser(apiResponse);
console.log(apiResponse.name); // ✅ string
}六、断言 vs 守卫对比
| 特性 | 类型断言 | 类型守卫 |
|---|---|---|
| 语法 | value as Type | typeof x === '...' |
| 运行时检查 | ❌ 不检查 | ✅ 检查 |
| 安全性 | ⚠️ 可能骗 TS | ✅ 安全 |
| 何时用 | 「我确定是这个类型」 | 「不确定,需要先判断」 |
typescript
// ❌ 用断言(不安全)
const value = getValue() as string;
value.toUpperCase(); // 运行时可能挂
// ✅ 用守卫(安全)
const value = getValue();
if (typeof value === 'string') {
value.toUpperCase(); // ✅ 一定安全
}七、常见错误
7.1 断言改写类型
typescript
// ❌ 你以为的转换
const x = '123' as unknown as number; // 编译过,运行挂
x.toFixed(2); // ❌ TypeError: x.toFixed is not a function
// ✅ 真正的转换
const x = Number('123'); // 123
x.toFixed(2); // ✅ "123.00"7.2 断言联合类型
typescript
// ❌ 编译过,运行炸
const value = getValue() as string;
if (typeof value === 'number') {
// 这里进不来因为断言成 string 了,逻辑漏洞
}7.3 滥用 as any
typescript
// ❌ 完全放弃类型
const value = something as any;
value.foo.bar.baz.qux; // 编译过,运行不知道
// ✅ 用 unknown 代替
const value: unknown = something;
if (typeof value === 'object' && value !== null) {
// 收窄后安全使用
}八、实战案例
8.1 表单输入校验
typescript
function validate(input: unknown): string {
if (typeof input !== 'string') {
throw new Error('必须是字符串');
}
if (input.length < 3) {
throw new Error('至少 3 个字符');
}
return input.trim();
}
const formValue = form.get('username') as string;
try {
const cleanValue = validate(formValue);
// ...
} catch (e) {
// 显示错误
}8.2 API 响应处理
typescript
interface UserResponse {
code: number;
data: User;
}
interface ApiError {
code: number;
message: string;
}
function isUserResponse(r: UserResponse | ApiError): r is UserResponse {
return 'data' in r;
}
async function fetchUser(id: number): Promise<User> {
const res: UserResponse | ApiError = await fetch(`/api/users/${id}`).then(r => r.json());
if (!isUserResponse(res)) {
throw new Error(res.message);
}
return res.data;
}8.3 状态库中的守卫
typescript
type State =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'success'; data: User }
| { kind: 'error'; error: Error };
function isIdle(s: State): s is { kind: 'idle' } {
return s.kind === 'idle';
}
function isSuccess(s: State): s is { kind: 'success'; data: User } {
return s.kind === 'success';
}
// 使用
if (isSuccess(state)) {
console.log(state.data.name); // ✅
}九、本章小结
| 要点 | 关键 |
|---|---|
| 类型断言 | value as Type,告诉 TS 你比它确定 |
| 类型守卫 | typeof / in / instanceof 收窄类型 |
| 类型谓词 | param is Type,自定义守卫函数 |
| 断言守卫 | asserts param is Type,失败抛错 |
| 最佳实践 | 优先用守卫,断言是最后手段 |
动手练习
- 类型守卫:写一个
isNonEmptyArray<T>(value: T[]): value is [T, ...T[]]守卫 - API 响应:定义
Success<T>和Failure类型,写一个isSuccess守卫 - 断言函数:写一个
assertPositive(n: number): asserts n is number函数
推荐阅读
- 📖 TypeScript Handbook - Type Assertions — 断言文档
- 📖 TypeScript Handbook - Narrowing — 收窄完整指南
- 🌐 TypeScript Assertion Functions — TS 3.7 引入
下一章:第 101 章:泛型(Generics) →