Skip to content
第 18 章 前端 ⏱ 14 分钟阅读

第 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() 或 lodash cloneDeep。

五、状态机的类型化 ​

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 返回

动手练习 ​

  1. HTTP 封装:实现一个 request<T>(url): Promise<T> 通用函数
  2. 事件总线:实现 bus.on('login', cb),cb 的参数类型自动推导
  3. 状态机:写 reducer,处理 FETCH / SUCCESS / ERROR 三种事件

下一章:第 19 章:常见坑速查 →

本站基于 VitePress 构建 · 由 StackHub 团队维护