第 18 章:类型实战
学习目标
- 用 TS 写一个 HTTP 客户端封装
- 实现类型安全的事件总线
- 构建表单校验的类型化方案
- 实战几个常见的"类型体操"
一、类型安全的 HTTP 客户端
typescript
// api.ts
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
class ApiError extends Error {
constructor(public code: number, message: string) {
super(message);
}
}
async function request<T>(
url: string,
init?: RequestInit,
): Promise<T> {
const res = await fetch(url, init);
if (!res.ok) {
throw new ApiError(res.status, res.statusText);
}
const json: ApiResponse<T> = await res.json();
if (json.code !== 0) {
throw new ApiError(json.code, json.message);
}
return json.data;
}
// 使用
interface User {
id: number;
name: string;
}
const user = await request<User>('/api/users/1');
user.id; // ✅
user.name; // ✅
user.age; // ❌二、类型安全的事件总线
typescript
type EventMap = {
login: { userId: string };
logout: { reason: string };
click: { x: number; y: number };
};
class EventBus {
private listeners: { [K in keyof EventMap]?: Array<(payload: EventMap[K]) => void> } = {};
on<K extends keyof EventMap>(event: K, cb: (payload: EventMap[K]) => void): void {
(this.listeners[event] ??= []).push(cb);
}
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
this.listeners[event]?.forEach(cb => cb(payload));
}
}
const bus = new EventBus();
bus.on('login', (p) => p.userId.toUpperCase()); // ✅ payload 推导正确
bus.emit('click', { x: 1, y: 2 });
bus.emit('login', { reason: 'x' }); // ❌ 字段错⚠️ 坑 1:事件总线没有反压机制,高频事件会堆积。生产环境用 RxJS / mitt。
三、表单校验的类型化
typescript
type Validator<T> = (value: T) => string | null;
type Rules<T> = {
[K in keyof T]?: Validator<T[K]>[];
};
function validate<T>(obj: T, rules: Rules<T>): Record<keyof T, string | null> {
const errors: any = {};
for (const key in rules) {
const validators = rules[key] ?? [];
for (const v of validators) {
const error = v(obj[key]);
if (error) {
errors[key] = error;
break;
}
}
}
return errors;
}
// 使用
interface Form {
name: string;
age: number;
}
const rules: Rules<Form> = {
name: [
(v) => (v.length === 0 ? '必填' : null),
(v) => (v.length > 10 ? '太长' : null),
],
age: [(v) => (v < 0 ? '不能为负' : null)],
};
const errors = validate({ name: 'Tom', age: 18 }, rules);四、类型化的 deepClone
typescript
function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item)) as T;
}
const result: any = {};
for (const key in obj) {
result[key] = deepClone((obj as any)[key]);
}
return result;
}⚠️ 坑 2:这版没处理
Date/Map/Set/RegExp,生产用structuredClone()或 lodashcloneDeep。
五、状态机的类型化
typescript
type State =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'success'; data: User[] }
| { kind: 'error'; msg: string };
type Event =
| { type: 'FETCH' }
| { type: 'SUCCESS'; data: User[] }
| { type: 'ERROR'; msg: string };
function reducer(state: State, event: Event): State {
switch (event.type) {
case 'FETCH':
return { kind: 'loading' };
case 'SUCCESS':
return { kind: 'success', data: event.data };
case 'ERROR':
return { kind: 'error', msg: event.msg };
}
}六、类型化路由(Vue Router 风格)
typescript
interface Route {
path: string;
component: () => Promise<{ default: any }>;
meta?: { auth?: boolean };
}
const routes: Route[] = [
{
path: '/users',
component: () => import('./views/Users.vue'),
meta: { auth: true },
},
{
path: '/login',
component: () => import('./views/Login.vue'),
},
];七、SQL 风格的查询构造器(简化版)
typescript
type Where = Record<string, string | number>;
interface Query {
where(conditions: Where): Query;
select<K extends string>(...keys: K[]): Query;
toString(): string;
}
function table(name: string): Query {
const conditions: Where = {};
const selects: string[] = ['*'];
return {
where(c) { Object.assign(conditions, c); return this; },
select(...keys) { selects.length = 0; selects.push(...keys); return this; },
toString() {
const where = Object.entries(conditions).map(([k, v]) => `${k}='${v}'`).join(' AND ');
return `SELECT ${selects.join(', ')} FROM ${name} WHERE ${where}`;
},
};
}
const q = table('users').where({ active: 1 }).select('id', 'name');
console.log(q.toString()); // SELECT id, name FROM users WHERE active='1'八、JSON Schema 推导类型
typescript
const userSchema = {
id: 'number',
name: 'string',
active: 'boolean',
} as const;
type FromSchema<T extends Record<string, 'number' | 'string' | 'boolean'>> = {
[K in keyof T]: T[K] extends 'number' ? number
: T[K] extends 'string' ? string
: boolean;
};
type User = FromSchema<typeof userSchema>;
// { id: number; name: string; active: boolean }九、本章小结
| 实战场景 | 关键类型技巧 |
|---|---|
| HTTP 客户端 | 泛型函数 + Promise<T> |
| 事件总线 | 类型映射 EventMap + keyof 约束 |
| 表单校验 | 映射类型 Rules<T> |
| 状态机 | 可辨识联合 kind + switch 收窄 |
| 路由 | 元数据 meta + 类型化配置 |
| 查询构造器 | 链式调用 + this 返回 |
动手练习
- HTTP 封装:实现一个
request<T>(url): Promise<T>通用函数 - 事件总线:实现
bus.on('login', cb),cb 的参数类型自动推导 - 状态机:写 reducer,处理
FETCH/SUCCESS/ERROR三种事件
下一章:第 19 章:常见坑速查 →