TypeScript 高级类型:联合、交叉、字面量类型与类型守卫
这一篇聊聊 TypeScript 的高级类型系统:联合类型、交叉类型、字面量类型,
以及如何使用类型守卫来安全地处理这些类型。
联合类型(Union Types)
联合类型表示一个值可以是多种类型中的一种,使用 | 连接。
基础用法
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}
交叉类型(Intersection Types)
交叉类型表示一个值必须同时满足多种类型,使用 & 连接。
基础用法
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;
字面量类型(Literal Types)
字面量类型是具体的值作为类型,而不是值的类型。
字符串字面量类型
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;
const 断言
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
类型守卫(Type Guards)
类型守卫是运行时检查,帮助 TypeScript 在特定代码块中收窄类型。
typeof 类型守卫
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}
instanceof 类型守卫
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}
in 操作符类型守卫
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}
可辨识联合(Discriminated Unions)
通过一个共同的属性(标签)来区分联合类型中的不同成员:
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}
类型别名 vs 接口
类型别名(Type Aliases)
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;
接口(Interfaces)
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; }
实际应用场景
API 响应类型
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 代码,
让类型系统成为开发时的有力助手,而不是负担。