TypeScript 高级类型:联合、交叉、字面量类型与类型守卫

这一篇聊聊 TypeScript 的高级类型系统:联合类型、交叉类型、字面量类型,
以及如何使用类型守卫来安全地处理这些类型。

联合类型表示一个值可以是多种类型中的一种,使用 | 连接。

 1// 一个变量可以是 string 或 number
 2let value: string | number;
 3value = 'hello';
 4value = 42;
 5// value = true; // 错误:boolean 不在联合类型中
 6
 7// 函数参数可以是多种类型
 8function format(value: string | number): string {
 9  return String(value);
10}

当使用联合类型时,TypeScript 需要你明确处理每种可能的类型:

 1function processValue(value: string | number) {
 2  // 错误:不能直接使用,因为 TypeScript 不知道具体是哪种类型
 3  // return value.toUpperCase(); // Property 'toUpperCase' does not exist on type 'number'
 4  
 5  // 需要先判断类型
 6  if (typeof value === 'string') {
 7    return value.toUpperCase(); // 这里 TypeScript 知道 value 是 string
 8  } else {
 9    return value.toFixed(2); // 这里 TypeScript 知道 value 是 number
10  }
11}
1// 数组元素可以是多种类型
2const items: (string | number)[] = ['hello', 42, 'world', 100];
3
4// 或者使用联合类型数组
5type StringOrNumber = string | number;
6const items2: StringOrNumber[] = ['hello', 42];
 1type Success = {
 2  status: 'success';
 3  data: string;
 4};
 5
 6type Error = {
 7  status: 'error';
 8  message: string;
 9};
10
11type Result = Success | Error;
12
13function handleResult(result: Result) {
14  if (result.status === 'success') {
15    console.log(result.data); // TypeScript 知道这里是 Success 类型
16  } else {
17    console.error(result.message); // TypeScript 知道这里是 Error 类型
18  }
19}

交叉类型表示一个值必须同时满足多种类型,使用 & 连接。

1type A = { a: string };
2type B = { b: number };
3type C = A & B; // C 必须同时有 a 和 b
4
5const obj: C = {
6  a: 'hello',
7  b: 42
8};
 1type User = {
 2  name: string;
 3  age: number;
 4};
 5
 6type Admin = {
 7  role: 'admin';
 8  permissions: string[];
 9};
10
11type AdminUser = User & Admin;
12
13const admin: AdminUser = {
14  name: 'John',
15  age: 30,
16  role: 'admin',
17  permissions: ['read', 'write']
18};
1type Func1 = (x: number) => number;
2type Func2 = (x: number) => string;
3
4// 交叉类型对于函数是取参数和返回值的交集
5type Func3 = Func1 & Func2;
6// Func3 要求函数同时满足 Func1 和 Func2
7// 但实际上这是不可能的(返回值不能既是 number 又是 string)
8// 所以 Func3 实际上是 never 类型
 1// 扩展对象类型
 2type Base = {
 3  id: string;
 4  createdAt: Date;
 5};
 6
 7type WithName = Base & {
 8  name: string;
 9};
10
11type WithEmail = Base & {
12  email: string;
13};
14
15type User = WithName & WithEmail;
16// User 必须有 id, createdAt, name, email
17
18// Mixin 模式
19type Timestamped = {
20  createdAt: Date;
21  updatedAt: Date;
22};
23
24type Identifiable = {
25  id: string;
26};
27
28type Entity = Timestamped & Identifiable;

字面量类型是具体的值作为类型,而不是值的类型。

 1// 变量只能是特定的字符串值
 2type Direction = 'up' | 'down' | 'left' | 'right';
 3
 4function move(direction: Direction) {
 5  console.log(`Moving ${direction}`);
 6}
 7
 8move('up');    // 正确
 9move('down');  // 正确
10// move('north'); // 错误:'north' 不在 Direction 类型中
11
12// 对象属性也可以是字面量类型
13type Status = 'pending' | 'success' | 'error';
14
15type Response = {
16  status: Status;
17  data?: string;
18};
1type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
2
3function rollDice(): DiceRoll {
4  return (Math.floor(Math.random() * 6) + 1) as DiceRoll;
5}
6
7// 常用于配置选项
8type LogLevel = 0 | 1 | 2 | 3;
1// 虽然不常用,但也是可能的
2type TrueOnly = true;
3type FalseOnly = false;

as const 可以让 TypeScript 推断出最具体的类型,包括字面量类型:

 1// 不使用 as const
 2const colors = ['red', 'green', 'blue'];
 3// colors 的类型是 string[]
 4
 5// 使用 as const
 6const colors2 = ['red', 'green', 'blue'] as const;
 7// colors2 的类型是 readonly ["red", "green", "blue"]
 8
 9// 对象使用 as const
10const config = {
11  api: 'https://api.example.com',
12  timeout: 5000,
13  retries: 3
14} as const;
15// config 的类型是 { readonly api: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3; }
16
17// 从 const 对象中提取字面量类型
18type ApiUrl = typeof config.api; // "https://api.example.com"
19type Timeout = typeof config.timeout; // 5000

类型守卫是运行时检查,帮助 TypeScript 在特定代码块中收窄类型。

1function process(value: string | number) {
2  if (typeof value === 'string') {
3    // 这里 TypeScript 知道 value 是 string
4    return value.toUpperCase();
5  }
6  // 这里 TypeScript 知道 value 是 number
7  return value.toFixed(2);
8}
 1class Dog {
 2  bark() {
 3    console.log('Woof!');
 4  }
 5}
 6
 7class Cat {
 8  meow() {
 9    console.log('Meow!');
10  }
11}
12
13function makeSound(animal: Dog | Cat) {
14  if (animal instanceof Dog) {
15    animal.bark(); // TypeScript 知道这里是 Dog
16  } else {
17    animal.meow(); // TypeScript 知道这里是 Cat
18  }
19}
 1type Fish = {
 2  swim: () => void;
 3};
 4
 5type Bird = {
 6  fly: () => void;
 7};
 8
 9function move(animal: Fish | Bird) {
10  if ('swim' in animal) {
11    animal.swim(); // TypeScript 知道这里是 Fish
12  } else {
13    animal.fly(); // TypeScript 知道这里是 Bird
14  }
15}
 1// 类型谓词(Type Predicate)
 2function isString(value: unknown): value is string {
 3  return typeof value === 'string';
 4}
 5
 6function isNumber(value: unknown): value is number {
 7  return typeof value === 'number' && !isNaN(value);
 8}
 9
10function processValue(value: unknown) {
11  if (isString(value)) {
12    // TypeScript 知道 value 是 string
13    return value.toUpperCase();
14  }
15  
16  if (isNumber(value)) {
17    // TypeScript 知道 value 是 number
18    return value.toFixed(2);
19  }
20  
21  throw new Error('Unknown value type');
22}
23
24// 更复杂的类型守卫
25interface User {
26  name: string;
27  email: string;
28}
29
30interface Admin {
31  name: string;
32  role: 'admin';
33  permissions: string[];
34}
35
36function isAdmin(user: User | Admin): user is Admin {
37  return 'role' in user && user.role === 'admin';
38}
39
40function handleUser(user: User | Admin) {
41  if (isAdmin(user)) {
42    // TypeScript 知道 user 是 Admin
43    console.log(user.permissions);
44  } else {
45    // TypeScript 知道 user 是 User
46    console.log(user.email);
47  }
48}

通过一个共同的属性(标签)来区分联合类型中的不同成员:

 1type Success = {
 2  type: 'success';
 3  data: string;
 4};
 5
 6type Error = {
 7  type: 'error';
 8  message: string;
 9};
10
11type Loading = {
12  type: 'loading';
13};
14
15type Result = Success | Error | Loading;
16
17function handleResult(result: Result) {
18  switch (result.type) {
19    case 'success':
20      console.log(result.data); // TypeScript 知道这里是 Success
21      break;
22    case 'error':
23      console.error(result.message); // TypeScript 知道这里是 Error
24      break;
25    case 'loading':
26      console.log('Loading...'); // TypeScript 知道这里是 Loading
27      break;
28  }
29}
1// 使用 type 定义类型别名
2type Point = {
3  x: number;
4  y: number;
5};
6
7type ID = string | number;
8
9type Callback = (error: Error | null, data?: string) => void;
 1// 使用 interface 定义接口
 2interface Point {
 3  x: number;
 4  y: number;
 5}
 6
 7interface User {
 8  name: string;
 9  age: number;
10}

类型别名可以:

  • 定义联合类型、交叉类型、元组
  • 使用计算属性名
  • 不能被重新声明(不能声明合并)
 1// 联合类型只能用 type
 2type Status = 'pending' | 'success' | 'error';
 3
 4// 元组
 5type Pair = [string, number];
 6
 7// 计算属性
 8type DynamicKey = {
 9  [key: string]: any;
10};

接口可以:

  • 被扩展和实现
  • 支持声明合并
  • 更符合面向对象编程习惯
 1// 接口扩展
 2interface Animal {
 3  name: string;
 4}
 5
 6interface Dog extends Animal {
 7  breed: string;
 8}
 9
10// 声明合并
11interface Window {
12  customProperty: string;
13}
14
15interface Window {
16  anotherProperty: number;
17}
18// 两个声明会合并

选择建议:

  • 需要联合类型、交叉类型、元组时,使用 type
  • 需要被类实现或扩展时,使用 interface
  • 需要声明合并时,使用 interface
  • 其他情况两者都可以,看个人偏好
 1// 字符串索引签名
 2type StringDictionary = {
 3  [key: string]: string;
 4};
 5
 6const dict: StringDictionary = {
 7  name: 'John',
 8  age: '30' // 注意:值必须是 string,不能是 number
 9};
10
11// 数字索引签名
12type NumberDictionary = {
13  [key: number]: string;
14};
15
16// 同时使用字符串和数字索引
17type MixedDictionary = {
18  [key: string]: string | number;
19  [key: number]: string; // 数字索引的值类型必须是字符串索引值类型的子类型
20};
1type ReadonlyDictionary = {
2  readonly [key: string]: string;
3};
4
5const readonlyDict: ReadonlyDictionary = {
6  name: 'John'
7};
8
9// readonlyDict.name = 'Jane'; // 错误:只读属性
 1// 将对象的所有属性变为可选
 2type Partial<T> = {
 3  [P in keyof T]?: T[P];
 4};
 5
 6// 将对象的所有属性变为只读
 7type Readonly<T> = {
 8  readonly [P in keyof T]: T[P];
 9};
10
11// 使用
12interface User {
13  name: string;
14  age: number;
15}
16
17type PartialUser = Partial<User>;
18// { name?: string; age?: number; }
19
20type ReadonlyUser = Readonly<User>;
21// { readonly name: string; readonly age: number; }
 1type ApiResponse<T> =
 2  | { status: 'success'; data: T }
 3  | { status: 'error'; message: string }
 4  | { status: 'loading' };
 5
 6async function fetchUser(id: string): Promise<ApiResponse<User>> {
 7  try {
 8    const response = await fetch(`/api/users/${id}`);
 9    if (response.ok) {
10      const data = await response.json();
11      return { status: 'success', data };
12    } else {
13      return { status: 'error', message: 'Failed to fetch user' };
14    }
15  } catch (error) {
16    return { status: 'error', message: error.message };
17  }
18}
19
20// 使用
21const result = await fetchUser('123');
22if (result.status === 'success') {
23  console.log(result.data); // TypeScript 知道这里有 data
24} else if (result.status === 'error') {
25  console.error(result.message); // TypeScript 知道这里有 message
26}
 1type Environment = 'development' | 'staging' | 'production';
 2
 3type Config = {
 4  env: Environment;
 5  apiUrl: string;
 6  timeout: number;
 7  features: {
 8    [key: string]: boolean;
 9  };
10};
11
12const config: Config = {
13  env: 'production',
14  apiUrl: 'https://api.example.com',
15  timeout: 5000,
16  features: {
17    darkMode: true,
18    analytics: false
19  }
20};

TypeScript 的高级类型系统提供了强大的类型表达能力:

  • 联合类型:表示"或"的关系,值可以是多种类型中的一种
  • 交叉类型:表示"且"的关系,值必须同时满足多种类型
  • 字面量类型:精确到具体值的类型,配合 as const 使用
  • 类型守卫:运行时检查,帮助 TypeScript 收窄类型
  • 可辨识联合:通过标签属性区分联合类型的不同成员
  • 类型别名 vs 接口:各有适用场景,根据需求选择

理解这些高级类型,可以写出更精确、更安全的 TypeScript 代码,
让类型系统成为开发时的有力助手,而不是负担。