TypeScript 实战技巧——断言、类型选型与 axios 封装

本文为「TypeScript 深入理解」系列第 4 篇,也是最后一篇。我们将前面三篇的类型知识落地到实际场景:类型断言、type vs interface 选型,以及一个完整的 axios 封装案例。

前面三篇把 TypeScript 的类型系统、面向对象、装饰器和编译原理都过了一遍。概念再多,最终都要落到代码上。这一篇聚焦三个实际问题:什么时候应该用类型断言?typeinterface 到底怎么选?如何在一个真实的 HTTP 请求封装中把泛型、接口、类型别名串起来?

类型断言:当你知道得比编译器多

类型断言告诉编译器「我知道这个变量的实际类型是什么」,相当于绕过编译器的类型推断。它不进行运行时转换,只影响编译期的类型判断。

两种语法:

1
2
3
4
5
6
// 1. 尖括号语法(在 JSX/TSX 中不可用)
let someValue: any = 'hello world';
let strLength: number = (<string>someValue).length;

// 2. as 语法(推荐,在所有环境下都可用)
let strLength2: number = (someValue as string).length;

常见场景:从 unknown 或联合类型中收窄类型,或访问 DOM 元素的特定属性。

1
2
3
4
5
6
7
8
9
// 场景一:收窄 unknown
function processValue(value: unknown) {
// value.foo(); // Error: unknown 类型不能直接调用方法
(value as string).toUpperCase();
}

// 场景二:DOM 操作
const input = document.getElementById('my-input') as HTMLInputElement;
input.value = 'hello';

类型断言不是类型转换,它不会改变运行时的值。如果断言的类型与实际类型不匹配,运行时不会报错但可能产生意外行为。更安全的做法是使用类型守卫(Type Guard):

1
2
3
4
5
function processValue(value: unknown) {
if (typeof value === 'string') {
value.toUpperCase(); // 类型已收窄为 string,不需要断言
}
}

type vs interface:什么时候用哪个

typeinterface 都能定义对象类型,很多场景下可以互换。但它们之间存在一些关键差异:

相同点:

两者都能描述对象形状、支持可选属性和只读属性、都允许函数签名定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
// type 写法
type User = {
readonly id: number;
name: string;
age?: number;
};

// interface 写法
interface User2 {
readonly id: number;
name: string;
age?: number;
}

不同点:

  1. interface 支持声明合并(同名接口会自动合并),type 不行:
1
2
3
4
5
6
7
8
interface Window {
myProp: string;
}
interface Window {
myProp2: number;
}
// 合并后 Window 同时拥有 myProp 和 myProp2
// 这在扩展第三方库类型时非常有用
  1. type 能定义联合类型、交叉类型、元组等,interface 不能:
1
2
3
type Status = 'idle' | 'loading' | 'success' | 'error';
type Point = [number, number];
type UserWithRole = User & { role: string };
  1. interface 面向对象风格更强,天然支持 extendsimplements
1
2
3
4
5
6
7
8
9
10
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
class Husky implements Dog {
name = 'Rex';
breed = 'Husky';
}

选择建议: 需要声明合并或描述对象/类的接口时用 interface;需要联合类型、交叉类型、映射类型等高级类型运算时用 type。团队统一风格即可,不必过度纠结。

案例:用 TypeScript 封装 axios

下面是一个实战案例——利用 TypeScript 对 axios 进行类型友好的封装。这个案例涵盖了本系列讨论的泛型、接口、类型别名、联合类型和类继承:用 type 定义请求方法和响应格式的联合类型,用泛型接口 CustomResponse<T> 让调用方拿到准确的响应数据类型,用基类 BaseHttp 封装通用的请求逻辑。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'

interface AxiosRequest {
baseURL?: string;
url: string;
data?: any;
params?: any;
method?: Method;
headers?: any;
timeout?: number;
responseType?: ResponseType;
}

interface CustomResponse<T> {
readonly status: boolean;
readonly message: string;
data: T;
origin?: any;
}


import axios, { AxiosRequestConfig } from 'axios';

// 定义接口
interface PendingType {
url?: string;
method?: Method;
params: any;
data: any;
cancel: Function;
}

// 取消重复请求
const pending: Array<PendingType> = [];
const CancelToken = axios.CancelToken;

// axios 实例
const instance = axios.create({
timeout: 10000,
responseType: 'json'
});

// 移除重复请求
const removePending = (config: AxiosRequestConfig) => {
for (const key in pending) {
const item: number = +key;
const list: PendingType = pending[key];
// 当前请求在数组中存在时执行函数体
if (list.url === config.url && list.method === config.method && JSON.stringify(list.params) === JSON.stringify(config.params) && JSON.stringify(list.data) === JSON.stringify(config.data)) {
// 执行取消操作
list.cancel('操作太频繁,请稍后再试');
// 从数组中移除记录
pending.splice(item, 1);
}
}
};

// 添加请求拦截器
instance.interceptors.request.use(
(request:any) => {
// TODO: handle loading

removePending(request);
request.cancelToken = new CancelToken((c) => {
pending.push({ url: request.url, method: request.method, params: request.params, data: request.data, cancel: c });
});
return request;
},
(error: any) => {
return Promise.reject(error);
}
);
// 添加响应拦截器
instance.interceptors.response.use(
(response: any) => {
removePending(response.config);

const errorCode = response?.data?.errorCode;
switch (errorCode) {
case '401':
// 根据errorCode,对业务做异常处理(和后端约定)
break;
default:
break;
}

return response;
},
(error: any) => {
const response = error.response;

// 根据返回的http状态码做不同的处理
switch (response?.status) {
case 401:
// token失效
break;
case 403:
// 没有权限
break;
case 500:
// 服务端错误
break;
case 503:
// 服务端错误
break;
default:
break;
}

return Promise.reject(response || {message: error.message});
}
);

class BaseHttp {
// 外部传入的baseUrl
// @ts-ignore
protected baseURL: string = process.env.VUE_APP_BaseURL as string;
// 自定义header头
protected headers: object = {
ContentType: 'application/json;charset=UTF-8'
}

private apiAxios<T>({
baseURL = this.baseURL,
headers = this.headers,
method,
url,
data,
params,
responseType
}: AxiosRequest): Promise<CustomResponse<T>> {

return new Promise((resolve, reject) => {
instance({
baseURL,
headers,
method,
url,
params,
data,
responseType
}).then((res: any) => {
// 200:服务端业务处理正常结束
if (res.status === 200) {
// TODO ...
// resolve({});
} else {
resolve({
status: false,
message: res.data?.errorMessage || (url + '请求失败'),
data: null
});
}
}).catch((err: any) => {
const message = err?.data?.errorMessage || err?.message || (url + '请求失败');
// eslint-disable-next-line
reject({ status: false, message, data: null});
});
});
}

/**
* GET类型的网络请求
*/
protected getReq<T>({
baseURL,
headers,
url,
data,
params,
responseType
}: AxiosRequest) {
return this.apiAxios<T>({
baseURL,
headers,
method: 'GET',
url,
data,
params,
responseType
});
}

/**
* POST类型的网络请求
*/
protected postReq<T>({ baseURL, headers, url, data, params, responseType }: AxiosRequest) {
return this.apiAxios<T>({ baseURL, headers, method: 'POST', url, data, params, responseType });
}

/**
* PUT类型的网络请求
*/
protected putReq<T>({ baseURL, headers, url, data, params, responseType }: AxiosRequest) {
return this.apiAxios<T>({ baseURL, headers, method: 'PUT', url, data, params, responseType });
}

/**
* DELETE类型的网络请求
*/
protected deleteReq<T>({ baseURL, headers, url, data, params, responseType }: AxiosRequest) {
return this.apiAxios<T>({ baseURL, headers, method: 'DELETE', url, data, params, responseType });
}
}

以上案例展示了 TypeScript 在实际项目中的典型应用模式:用类型别名固化业务常量、用接口约束数据结构、用泛型让 API 响应的类型在调用链中保持准确。整个封装的核心理念是「让类型帮你写文档」——调用方拿到 getReq<User[]> 这样的方法签名,一眼就知道返回的数据结构,不需要翻看接口文档。


「TypeScript 深入理解」系列到此结束。四篇文章覆盖了从基础类型到装饰器再到编译原理的完整知识链。TypeScript 的学习曲线不算平缓,但一旦你习惯了”先写类型再写逻辑”的思维方式,回头看纯 JavaScript 项目时,会觉得少了点什么。