文章

类型守卫的实现方式深度解析

深入解析TypeScript类型守卫的typeof、instanceof、in守卫及自定义类型谓词is的实现方式

类型守卫的实现方式深度解析

一句话概括

类型守卫是TypeScript中用于在运行时精准缩小联合类型范围的核心机制,通过typeofinstanceofin和自定义类型谓词is实现编译时的类型安全窄化。

背景

在TypeScript开发中,我们经常遇到联合类型(Union Types)的场景。例如,一个函数可能接受string | number类型的参数,但在实际处理逻辑中,我们需要针对不同类型执行不同的操作。这时就会产生一个问题:TypeScript在编译时如何确定联合类型的具体类型?

1
2
3
4
function processValue(value: string | number) {
  // 错误:无法直接调用字符串方法
  console.log(value.toUpperCase()); // ❌ 编译错误
}

传统解决方案是使用类型断言(Type Assertion):

1
2
3
4
5
function processValue(value: string | number) {
  if (typeof value === 'string') {
    console.log((value as string).toUpperCase()); // ✅ 但冗长且不安全
  }
}

这种方式存在两个问题:

  1. 冗余代码:每次使用都需要重复类型断言
  2. 安全隐患:类型断言不会在运行时验证,可能隐藏错误

类型守卫(Type Guards) 应运而生,它能够在条件判断中自动缩窄类型范围,让TypeScript编译器”智能”地识别出当前作用域中的具体类型。

在面试中,类型守卫是TypeScript高级特性的必考点,直接考察候选人对类型系统原理的理解深度。

系列衔接:上一篇我们深入探讨了《映射类型的原理》,理解了如何通过映射类型实现类型的批量转换;本篇将聚焦类型守卫,解决联合类型的运行时类型识别问题;下一篇《infer关键字深入》将进一步探索类型推断的高级应用。

概念与定义

什么是类型守卫?

类型守卫(Type Guard) 是一种表达式,当在条件判断中使用时,能够将联合类型窄化(Narrowing)到更具体的类型。

核心作用

  • 类型窄化:将宽类型(如string | number)缩窄为具体类型(如string
  • 编译时安全:在编译阶段提供类型检查,避免运行时错误
  • 代码简洁:消除冗余的类型断言,提升代码可读性

工作原理: 类型守卫通过控制流分析(Control Flow Analysis)实现。当TypeScript编译器遇到类型守卫表达式时,会在条件为true的分支中自动更新变量的类型信息。

1
2
3
4
5
6
7
8
9
10
// 类型窄化的直观示例
function example(value: string | number) {
  if (typeof value === 'string') {
    // 此作用域内,value 被窄化为 string 类型
    value.toUpperCase(); // ✅ 类型安全
  } else {
    // 此作用域内,value 被窄化为 number 类型
    value.toFixed(2); // ✅ 类型安全
  }
}

最小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 最小可运行示例:typeof 类型守卫
function formatValue(value: string | number): string {
  if (typeof value === 'string') {
    // typeof 守卫:value 在此作用域被窄化为 string
    return value.toUpperCase();
  } else {
    // 自动推断:value 在此作用域被窄化为 number
    return value.toFixed(2);
  }
}

console.log(formatValue("hello")); // 输出: HELLO
console.log(formatValue(3.14159)); // 输出: 3.14

核心知识点拆解

1. typeof 守卫

typeof 守卫用于判断基本数据类型(stringnumberbooleansymbolundefinedfunction)。

语法

1
2
3
if (typeof variable === 'typeString') {
  // variable 在此作用域被窄化为对应类型
}

支持的类型字符串

  • 'string'
  • 'number'
  • 'boolean'
  • 'symbol'
  • 'undefined'
  • 'function'
  • 'object'(注意:null 也会返回 'object'

示例

1
2
3
4
5
6
7
8
9
10
11
12
function processInput(input: string | number | boolean) {
  if (typeof input === 'string') {
    // input: string
    console.log(input.length);
  } else if (typeof input === 'number') {
    // input: number
    console.log(input.toFixed(2));
  } else {
    // input: boolean
    console.log(input ? 'true' : 'false');
  }
}

局限性

  • 无法区分自定义对象类型(如 typeof obj 总是返回 'object'
  • 无法检测 null(会错误返回 'object'
1
2
3
4
5
6
function handleValue(value: string | null) {
  if (typeof value === 'object') {
    // ⚠️ 危险:null 也会进入此分支
    console.log(value.toUpperCase()); // ❌ 运行时错误
  }
}

2. instanceof 守卫

instanceof 守卫用于检查对象是否是特定类的实例,适用于自定义类、内置对象(如 DateRegExpArray 等)。

语法

1
2
3
if (variable instanceof ClassName) {
  // variable 在此作用域被窄化为 ClassName 类型
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal {
  move() { console.log('Moving...'); }
}

class Dog extends Animal {
  bark() { console.log('Woof!'); }
}

class Cat extends Animal {
  meow() { console.log('Meow!'); }
}

function handleAnimal(animal: Animal) {
  if (animal instanceof Dog) {
    // animal: Dog
    animal.bark();
  } else if (animal instanceof Cat) {
    // animal: Cat
    animal.meow();
  } else {
    // animal: Animal
    animal.move();
  }
}

高级用法:自定义类与接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 接口无法直接用 instanceof 检查,需要类实现
interface Shape {
  area(): number;
}

class Circle implements Shape {
  constructor(public radius: number) {}
  area() { return Math.PI * this.radius ** 2; }
}

class Rectangle implements Shape {
  constructor(public width: number, public height: number) {}
  area() { return this.width * this.height; }
}

function printArea(shape: Shape) {
  if (shape instanceof Circle) {
    console.log(`圆形面积: ${shape.area()}, 半径: ${shape.radius}`);
  } else if (shape instanceof Rectangle) {
    console.log(`矩形面积: ${shape.area()}, 宽: ${shape.width}, 高: ${shape.height}`);
  }
}

注意事项

  • instanceof 检查原型链,对于复杂继承关系可能产生意外结果
  • 无法用于检查基本类型(stringnumber 等)

3. in 守卫

in 守卫用于检查对象是否包含特定属性,适用于区分具有不同属性签名的对象类型。

语法

1
2
3
if ('propertyName' in variable) {
  // variable 在此作用域被窄化为包含该属性的类型
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface User {
  id: number;
  name: string;
  email: string;
}

interface Admin {
  id: number;
  name: string;
  permissions: string[];
}

function handlePerson(person: User | Admin) {
  if ('email' in person) {
    // person: User
    console.log(`用户邮箱: ${person.email}`);
  } else if ('permissions' in person) {
    // person: Admin
    console.log(`管理员权限: ${person.permissions.join(', ')}`);
  }
}

高级用法:嵌套属性检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface ApiResponseSuccess {
  status: 'success';
  data: any;
}

interface ApiResponseError {
  status: 'error';
  message: string;
  code: number;
}

function handleResponse(response: ApiResponseSuccess | ApiResponseError) {
  if ('data' in response) {
    // response: ApiResponseSuccess
    console.log('请求成功:', response.data);
  } else {
    // response: ApiResponseError
    console.log(`请求失败 [${response.code}]: ${response.message}`);
  }
}

技巧:结合属性存在性进行多重检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function getSmallPet(pet: Bird | Fish) {
  if ('fly' in pet) {
    // pet: Bird
    pet.fly();
  } else {
    // pet: Fish
    pet.swim();
  }
  
  // 共同属性无需类型守卫
  pet.layEggs();
}

4. 自定义类型谓词(Type Predicate)— is 关键字

当内置类型守卫无法满足需求时,可以使用自定义类型谓词(Type Predicate)创建灵活的类型守卫函数。

语法

1
2
3
function isTypeName(variable: any): variable is TypeName {
  // 返回 boolean 的逻辑判断
}

核心要点

  • 返回类型必须是 parameterName is Type 形式
  • 函数体返回 boolean
  • TypeScript 会在条件为 true 时窄化类型

基础示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Bird {
  fly(): void;
}

interface Fish {
  swim(): void;
}

// 自定义类型守卫函数
function isFish(pet: Bird | Fish): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Bird | Fish) {
  if (isFish(pet)) {
    // pet: Fish
    pet.swim();
  } else {
    // pet: Bird
    pet.fly();
  }
}

高级应用:复杂对象验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface UserProfile {
  id: number;
  name: string;
  email: string;
  age?: number;
}

// 运行时类型验证 + 类型守卫
function isUserProfile(obj: any): obj is UserProfile {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof obj.id === 'number' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string' &&
    (obj.age === undefined || typeof obj.age === 'number')
  );
}

function processProfile(data: any) {
  if (isUserProfile(data)) {
    // data: UserProfile
    console.log(`用户: ${data.name}, 邮箱: ${data.email}`);
  } else {
    console.error('无效的用户数据');
  }
}

泛型类型谓词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 通用数组类型检查
function isArrayOf<T>(
  arr: any,
  checkFn: (item: any) => item is T
): arr is T[] {
  return Array.isArray(arr) && arr.every(item => checkFn(item));
}

// 使用示例
const mixedData: any[] = [1, 2, 3, 4];

if (isArrayOf(mixedData, (item): item is number => typeof item === 'number')) {
  // mixedData: number[]
  const sum = mixedData.reduce((a, b) => a + b, 0);
  console.log(`数字总和: ${sum}`);
}

5. 区分类型守卫与类型断言(as)

类型断言(Type Assertion)

1
2
3
4
5
function processValue(value: string | number) {
  // 强制告诉编译器值的类型,无运行时检查
  const str = value as string; // ⚠️ 危险:如果 value 是 number,运行时可能出错
  console.log(str.toUpperCase());
}

类型守卫(Type Guard)

1
2
3
4
5
6
7
function processValue(value: string | number) {
  // 通过条件判断,编译器自动窄化类型
  if (typeof value === 'string') {
    // 安全:value 已被确认为 string
    console.log(value.toUpperCase());
  }
}

核心区别对比表

特性类型守卫类型断言
运行时检查✅ 有❌ 无
安全性✅ 编译时+运行时安全⚠️ 仅编译时,可能运行时错误
代码冗余✅ 无冗余❌ 每次使用需重复断言
适用场景条件分支中的类型窄化确定类型但编译器无法推断时
性能✅ 有运行时开销(条件判断)✅ 无运行时开销

最佳实践

  • 优先使用类型守卫,确保类型安全
  • 仅在确定类型正确且无法使用类型守卫时使用类型断言
  • 避免双重断言(value as any as string),这通常是设计问题的信号

6. 可辨识联合(Discriminated Union)与类型守卫

可辨识联合(Discriminated Union) 是一种通过公共字面量属性区分联合类型的高级模式,配合类型守卫可实现完美的类型窄化。

核心思想

  1. 联合类型的每个成员都有一个共同的属性(称为”判别属性”)
  2. 该属性的类型为字面量类型'success' | 'error'
  3. 通过检查判别属性,TypeScript 能自动窄化类型

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义可辨识联合类型
type ApiResponse =
  | { status: 'success'; data: any; timestamp: number }
  | { status: 'error'; message: string; code: number }
  | { status: 'loading' };

function handleResponse(response: ApiResponse) {
  // 检查判别属性 status
  if (response.status === 'success') {
    // response: { status: 'success'; data: any; timestamp: number }
    console.log('数据:', response.data);
    console.log('时间戳:', new Date(response.timestamp).toLocaleString());
  } else if (response.status === 'error') {
    // response: { status: 'error'; message: string; code: number }
    console.error(`错误 [${response.code}]: ${response.message}`);
  } else {
    // response: { status: 'loading' }
    console.log('加载中...');
  }
}

高级模式:嵌套可辨识联合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      // shape: { kind: 'circle'; radius: number }
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      // shape: { kind: 'rectangle'; width: number; height: number }
      return shape.width * shape.height;
    case 'triangle':
      // shape: { kind: 'triangle'; base: number; height: number }
      return 0.5 * shape.base * shape.height;
  }
}

exhaustive check(穷尽检查)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用 never 类型确保处理所有联合类型分支
function assertNever(x: never): never {
  throw new Error('Unexpected value: ' + x);
}

function getArea(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:
      // 如果遗漏某个分支,这里会报错
      return assertNever(shape);
  }
}

实战案例

案例一:实现一个类型安全的 HTTP 请求函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 定义可辨识联合类型的响应
type HttpResponse<T> =
  | { status: 'success'; data: T; statusCode: number }
  | { status: 'error'; message: string; statusCode: number }
  | { status: 'loading' }
  | { status: 'idle' };

// 自定义类型守卫
function isSuccessResponse<T>(
  response: HttpResponse<T>
): response is { status: 'success'; data: T; statusCode: number } {
  return response.status === 'success';
}

function isErrorResponse<T>(
  response: HttpResponse<T>
): response is { status: 'error'; message: string; statusCode: number } {
  return response.status === 'error';
}

// 类型安全的 HTTP 请求函数
async function fetchData<T>(url: string): Promise<HttpResponse<T>> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      return {
        status: 'error',
        message: response.statusText,
        statusCode: response.status
      };
    }
    const data: T = await response.json();
    return {
      status: 'success',
      data,
      statusCode: response.status
    };
  } catch (error) {
    return {
      status: 'error',
      message: error instanceof Error ? error.message : 'Unknown error',
      statusCode: 0
    };
  }
}

// 使用示例
interface User {
  id: number;
  name: string;
}

async function loadUser() {
  const response = await fetchData<User>('https://api.example.com/user/1');
  
  if (isSuccessResponse(response)) {
    // response: { status: 'success'; data: User; statusCode: number }
    console.log(`用户: ${response.data.name}`);
  } else if (isErrorResponse(response)) {
    // response: { status: 'error'; message: string; statusCode: number }
    console.error(`请求失败: ${response.message}`);
  }
}

案例二:实现表单校验的类型守卫链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 定义表单字段类型
type FormField =
  | { type: 'text'; value: string; minLength?: number }
  | { type: 'number'; value: number; min?: number; max?: number }
  | { type: 'email'; value: string }
  | { type: 'checkbox'; checked: boolean };

// 类型守卫链
function validateField(field: FormField): string[] {
  const errors: string[] = [];
  
  switch (field.type) {
    case 'text':
      // field: { type: 'text'; value: string; minLength?: number }
      if (field.minLength && field.value.length < field.minLength) {
        errors.push(`文本长度不能少于 ${field.minLength} 个字符`);
      }
      break;
      
    case 'number':
      // field: { type: 'number'; value: number; min?: number; max?: number }
      if (field.min !== undefined && field.value < field.min) {
        errors.push(`数值不能小于 ${field.min}`);
      }
      if (field.max !== undefined && field.value > field.max) {
        errors.push(`数值不能大于 ${field.max}`);
      }
      break;
      
    case 'email':
      // field: { type: 'email'; value: string }
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(field.value)) {
        errors.push('邮箱格式不正确');
      }
      break;
      
    case 'checkbox':
      // field: { type: 'checkbox'; checked: boolean }
      if (!field.checked) {
        errors.push('必须勾选同意条款');
      }
      break;
  }
  
  return errors;
}

// 使用示例
const formFields: FormField[] = [
  { type: 'text', value: 'Hello', minLength: 10 },
  { type: 'number', value: 150, min: 0, max: 100 },
  { type: 'email', value: 'invalid-email' },
  { type: 'checkbox', checked: false }
];

formFields.forEach((field, index) => {
  const errors = validateField(field);
  if (errors.length > 0) {
    console.log(`字段 ${index + 1} 校验失败:`, errors);
  }
});

案例三:实现可辨识联合状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 定义状态机的状态和事件
type State =
  | { status: 'idle' }
  | { status: 'loading'; startTime: number }
  | { status: 'success'; data: string; fetchedAt: number }
  | { status: 'error'; error: string; retryCount: number };

type Event =
  | { type: 'FETCH' }
  | { type: 'SUCCESS'; data: string }
  | { type: 'FAILURE'; error: string }
  | { type: 'RETRY' }
  | { type: 'RESET' };

// 状态机转换器
function reducer(state: State, event: Event): State {
  switch (state.status) {
    case 'idle':
      if (event.type === 'FETCH') {
        return { status: 'loading', startTime: Date.now() };
      }
      return state;
      
    case 'loading':
      if (event.type === 'SUCCESS') {
        return {
          status: 'success',
          data: event.data,
          fetchedAt: Date.now()
        };
      }
      if (event.type === 'FAILURE') {
        return {
          status: 'error',
          error: event.error,
          retryCount: 0
        };
      }
      return state;
      
    case 'success':
      if (event.type === 'RESET') {
        return { status: 'idle' };
      }
      return state;
      
    case 'error':
      if (event.type === 'RETRY') {
        return { status: 'loading', startTime: Date.now() };
      }
      if (event.type === 'RESET') {
        return { status: 'idle' };
      }
      return state;
      
    default:
      // 穷尽检查
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck;
  }
}

// 使用示例
let currentState: State = { status: 'idle' };

currentState = reducer(currentState, { type: 'FETCH' });
console.log('当前状态:', currentState);
// 输出: { status: 'loading', startTime: 1620000000000 }

currentState = reducer(currentState, { type: 'SUCCESS', data: 'Hello World' });
console.log('当前状态:', currentState);
// 输出: { status: 'success', data: 'Hello World', fetchedAt: 1620000001000 }

底层原理

编译时窄化机制

TypeScript 的类型守卫依赖于控制流分析(Control Flow Analysis)

  1. 类型窄化(Type Narrowing)
    • 当 TypeScript 编译器遇到类型守卫表达式时,会在条件为 true 的代码路径中更新变量的类型信息
    • 窄化是局部作用域内的临时类型推断,不会影响原始声明
  2. 类型收缩算法: ``` 输入: 联合类型 T = A | B | C 守卫: condition x => x is A 输出:
    • 若 condition 为 true: 类型收缩为 A
    • 若 condition 为 false: 类型收缩为 B | C ```

类型谓词的推断过程

当函数返回类型为 parameter is Type 时,TypeScript 编译器:

  1. 标记函数为类型谓词函数
  2. 在条件判断中,如果该函数在 if 语句中被调用且返回 true,则:
    • 将参数类型窄化为 Type
    • 更新当前作用域的类型上下文
  3. 类型谓词优先于函数体的实际返回值类型检查
1
2
3
4
5
6
7
8
// 编译器的推断过程示例
function isString(value: any): value is string {
  return typeof value === 'string';
}

// 编译器内部处理:
// 1. 检查 isString 的返回类型是否为类型谓词
// 2. 如果是,则在 if (isString(x)) 分支中,将 x 的类型窄化为 string

Control Flow Analysis 详解

TypeScript 使用数据流分析追踪每个变量的类型变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function example(x: string | number) {
  // 初始: x: string | number
  
  if (typeof x === 'string') {
    // 分支1: x: string
    console.log(x.toUpperCase());
  } else {
    // 分支2: x: number
    console.log(x.toFixed(2));
  }
  
  // 合并点: x: string | number(类型重新宽化)
  console.log(x);
}

分析步骤

  1. 初始化:变量 x 的类型为声明类型 string | number
  2. 分支分析:在 if 分支中,x 的类型被窄化为 string;在 else 分支中,窄化为 number
  3. 类型合并:在条件块之后,TypeScript 合并所有分支的类型,恢复为原始联合类型

💡 人话总结: 类型守卫就像是一个”类型侦探”,在代码执行的过程中,通过 typeofinstanceofin 这些”线索”,智能判断变量到底是什么类型。它让 TypeScript 编译器能够”看懂”你的条件判断,自动帮你缩小类型范围,避免你手动写一大堆类型断言。自定义类型谓词 is 则是让你自己定义”侦探规则”,告诉编译器:”相信我,如果这个函数返回 true,那这个变量一定是这个类型!”

高频面试题解析

Q1:TypeScript 中有哪些类型守卫方式?

答案: TypeScript 提供以下几种类型守卫方式:

  1. typeof 守卫:用于基本数据类型检查
    1
    
    if (typeof value === 'string') { /* ... */ }
    
  2. instanceof 守卫:用于自定义类或内置对象检查
    1
    
    if (value instanceof Date) { /* ... */ }
    
  3. in 守卫:用于对象属性存在性检查
    1
    
    if ('property' in object) { /* ... */ }
    
  4. 自定义类型谓词(is):灵活的用户自定义守卫
    1
    
    function isType(value: any): value is Type { /* ... */ }
    
  5. 可辨识联合(Discriminated Union):通过公共字面量属性区分类型
    1
    
    if (response.status === 'success') { /* ... */ }
    
  6. 相等性缩小(Equality Narrowing)
    1
    
    if (value === 'literal') { /* value 被窄化为 'literal' */ }
    

Q2:类型谓词 is 和类型断言 as 有什么区别?

答案

对比维度类型谓词 is类型断言 as
运行时检查✅ 有(函数体内实现)❌ 无
安全性✅ 高(编译时+运行时)⚠️ 低(仅编译时)
使用场景条件判断中的类型窄化确定类型但编译器无法推断
代码复用✅ 可封装为函数复用❌ 每次使用需重复断言
错误处理✅ 可在函数内统一处理❌ 需在使用处分别处理

示例对比

1
2
3
4
5
6
7
8
9
10
11
// 类型谓词:安全且可复用
function isString(value: any): value is string {
  return typeof value === 'string';
}

if (isString(maybeString)) {
  // 安全:maybeString 已被确认为 string
}

// 类型断言:不安全且冗余
const str = maybeString as string; // ⚠️ 如果 maybeString 不是 string,运行时可能出错

Q3:什么是可辨识联合类型?为什么配合类型守卫使用?

答案

可辨识联合类型是一种通过公共判别属性(通常为字面量类型)区分的联合类型。

优势

  1. 类型安全:编译时确保处理所有联合成员
  2. 代码清晰:通过判别属性快速识别类型
  3. 自动窄化:TypeScript 能根据判别属性自动窄化类型

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
type Result =
  | { status: 'success'; data: any }
  | { status: 'error'; message: string };

function handleResult(result: Result) {
  if (result.status === 'success') {
    // result 自动窄化为 { status: 'success'; data: any }
    console.log(result.data);
  } else {
    // result 自动窄化为 { status: 'error'; message: string }
    console.error(result.message);
  }
}

为什么不配合类型守卫? 实际上,可辨识联合就是一种类型守卫模式。result.status === 'success' 本身就是一个类型守卫,TypeScript 能根据这个条件自动窄化类型。

Q4:如何实现一个数组类型过滤函数?

答案

使用类型谓词 is 可以实现类型安全的数组过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 基础版本:过滤出所有数字
function filterNumbers(arr: (number | string)[]): number[] {
  return arr.filter((item): item is number => typeof item === 'number');
}

// 通用版本:类型守卫过滤
function filterByType<T>(
  arr: any[],
  guard: (item: any) => item is T
): T[] {
  return arr.filter(guard);
}

// 使用示例
const mixedArray = [1, 'hello', 2, 'world', 3];

const numbers = filterByType(mixedArray, (item): item is number => 
  typeof item === 'number'
);
console.log(numbers); // 输出: [1, 2, 3]

const strings = filterByType(mixedArray, (item): item is string =>
  typeof item === 'string'
);
console.log(strings); // 输出: ['hello', 'world']

高级应用:多个类型守卫组合

1
2
3
4
5
6
7
function isNonNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

const arrayWithNulls = [1, null, 2, undefined, 3];
const nonNullNumbers = arrayWithNulls.filter(isNonNull);
console.log(nonNullNumbers); // 输出: [1, 2, 3]

总结与扩展

核心要点

  1. 类型守卫是 TypeScript 类型系统的核心特性,通过编译时类型窄化提升代码安全性
  2. 四种主要守卫方式
    • typeof:基本类型检查
    • instanceof:对象实例检查
    • in:属性存在性检查
    • 自定义类型谓词 is:灵活的用户定义守卫
  3. 可辨识联合是类型守卫的最佳实践,通过公共判别属性实现类型自动窄化
  4. 类型守卫 vs 类型断言:优先使用类型守卫,确保类型安全
  5. 控制流分析是类型守卫的底层实现机制,通过追踪代码执行路径实现类型窄化

延伸学习方向

  1. 高级类型编程
    • 条件类型(Conditional Types)
    • 映射类型(Mapped Types)← 上一篇主题
    • 类型推断(infer 关键字)← 下一篇主题
  2. 类型守卫的边界情况
    • 自定义类型守卫的性能优化
    • 复杂嵌套对象的类型守卫
    • 异步类型守卫的实现
  3. 实际应用
    • 在 React 组件中使用类型守卫
    • 在 Node.js 中处理不确定类型的输入
    • 构建类型安全的 API 客户端

相关主题


参考资料

  1. TypeScript 官方文档 - Type Guards and Differentiating Types
  2. 《Programming TypeScript》 by Boris Cherny
  3. TypeScript Deep Dive - Type Guards
  4. Microsoft TypeScript GitHub - Control Flow Analysis

工具推荐

  • TypeScript Playground:在线测试类型守卫效果
  • TSConfig 参考:strictNullChecks 对类型守卫的影响
  • ESLint 规则:@typescript-eslint/strict-boolean-expressions

作者注:类型守卫是 TypeScript 中最实用且最容易忽视的高级特性之一。掌握它不仅能提升代码质量,还能在面试中展现你对类型系统的深刻理解。

本文由作者按照 CC BY 4.0 进行授权