前端开发··2 阅读·预计 18 分钟

TypeScript 类型体操实战:手把手实现 DeepReadonly、DeepPartial 和类型安全的事件系统

在实际项目中,TypeScript 的类型系统远比你想象的更强大。今天通过三个实战场景,带你掌握高级类型编程。

为什么需要深度类型工具?

React 的状态管理、Node.js 的配置对象、Vue 的响应式系统——我们每天都在和嵌套对象打交道。内置的 Readonly<T> 只能处理一层,嵌套对象依然可变。

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      pass: string;
    };
  };
}

const config: Readonly<Config> = {
  database: {
    host: 'localhost',
    port: 5432,
    credentials: { user: 'root', pass: 'secret' }
  }
};

// ❌ 编译报错 — 这是预期的
config.database = { host: 'other', port: 3306, credentials: { user: '', pass: '' } };

// ❌ 但这个不会报错!只读是浅层的
config.database.port = 3306;
config.database.credentials.pass = 'hacked';

这就是我们需要 DeepReadonly 的原因。

实现 DeepReadonly

核心思路:递归遍历每个属性,对对象类型递归应用,对基本类型保持不变。

type DeepReadonly<T> = T extends (...args: any[]) => any
  ? T  // 函数保持不变
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

// 测试
type ReadonlyConfig = DeepReadonly<Config>;

const safeConfig: ReadonlyConfig = {
  database: {
    host: 'localhost',
    port: 5432,
    credentials: { user: 'root', pass: 'secret' }
  }
};

// ✅ 现在所有层级都是只读的
// safeConfig.database.port = 3306;        // 编译报错
// safeConfig.database.credentials.pass = 'x'; // 编译报错

处理边界情况

数组和 Map/Set 需要特殊处理:

type DeepReadonly<T> =
  T extends (...args: any[]) => any ? T
  : T extends Map<infer K, infer V> ? ReadonlyMap<K, V>
  : T extends Set<infer V> ? ReadonlySet<V>
  : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>>
  : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

实现 DeepPartial

DeepPartialDeepReadonly 的镜像——让嵌套对象的所有属性变为可选:

type DeepPartial<T> = T extends (...args: any[]) => any
  ? T
  : T extends object
    ? { [K in keyof T]?: DeepPartial<T[K]> }
    : T;

// 实战场景:配置合并
function mergeConfig<T>(defaults: T, overrides: DeepPartial<T>): T {
  const result = { ...defaults };
  for (const key of Object.keys(overrides) as Array<keyof T>) {
    const overrideVal = overrides[key];
    const defaultVal = defaults[key];
    if (
      overrideVal !== null &&
      typeof overrideVal === 'object' &&
      !Array.isArray(overrideVal) &&
      typeof defaultVal === 'object'
    ) {
      (result as any)[key] = mergeConfig(defaultVal as any, overrideVal as any);
    } else if (overrideVal !== undefined) {
      (result as any)[key] = overrideVal;
    }
  }
  return result;
}

// 使用
const defaults: Config = {
  database: { host: 'localhost', port: 5432, credentials: { user: 'root', pass: '' } }
};

const userOverrides: DeepPartial<Config> = {
  database: { port: 3306 }  // 只覆盖 port,其他保持默认
};

const final = mergeConfig(defaults, userOverrides);
// final.database.host = 'localhost' (默认值)
// final.database.port = 3306 (覆盖值)

类型安全的事件系统

这是最有实战价值的部分。在 Vue/React 项目中,事件总线或 PubSub 模式非常常见,但大多没有类型安全。

// 定义事件映射
interface AppEvents {
  'user:login': { userId: string; timestamp: number };
  'user:logout': { userId: string };
  'notification:show': { message: string; level: 'info' | 'warn' | 'error' };
  'cart:update': { items: Array<{ id: string; qty: number }> };
}

// 类型安全的 EventEmitter
class TypedEmitter<E extends Record<string, any>> {
  private listeners = new Map<keyof E, Set<Function>>();

  on<K extends keyof E>(event: K, handler: (payload: E[K]) => void): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(handler);

    // 返回取消监听的函数
    return () => {
      this.listeners.get(event)?.delete(handler);
    };
  }

  emit<K extends keyof E>(event: K, payload: E[K]): void {
    this.listeners.get(event)?.forEach(fn => fn(payload));
  }

  off<K extends keyof E>(event: K, handler: (payload: E[K]) => void): void {
    this.listeners.get(event)?.delete(handler);
  }
}

// 使用
const bus = new TypedEmitter<AppEvents>();

// ✅ 完整的类型提示和检查
const unsub = bus.on('user:login', (payload) => {
  console.log(payload.userId);      // ✅ payload 类型是 { userId: string; timestamp: number }
  console.log(payload.timestamp);   // ✅ 自动补全
  // payload.level  // ❌ 编译报错,这个事件没有 level
});

// ✅ emit 同样有类型检查
bus.emit('notification:show', { message: 'Hello', level: 'info' });
// bus.emit('notification:show', { message: 'Hi', level: 'critical' });  // ❌ 'critical' 不在联合类型中

// ✅ 返回的取消函数可以直接调用
unsub();

集成到 Vue 3 组合式 API

import { onUnmounted, ref, Ref } from 'vue';

function useTypedEventBus<E extends Record<string, any>>(bus: TypedEmitter<E>) {
  const cleanups: Array<() => void> = [];

  function subscribe<K extends keyof E>(
    event: K,
    handler: (payload: E[K]) => void
  ): Ref<boolean> {
    const active = ref(true);
    const unsub = bus.on(event, handler);
    cleanups.push(() => {
      unsub();
      active.value = false;
    });
    return active;
  }

  onUnmounted(() => {
    cleanups.forEach(fn => fn());
    cleanups.length = 0;
  });

  return { subscribe, emit: bus.emit.bind(bus) };
}

// 组件中使用
export default {
  setup() {
    const { subscribe, emit } = useTypedEventBus(bus);

    subscribe('user:login', ({ userId, timestamp }) => {
      console.log(`User ${userId} logged in at ${timestamp}`);
    });

    return { login: () => emit('user:login', { userId: 'u1', timestamp: Date.now() }) };
  }
};

集成到 Node.js Koa 服务端

import Koa from 'koa';

interface ServerEvents {
  'request:received': { method: string; path: string; ip: string };
  'request:completed': { method: string; path: string; status: number; durationMs: number };
  'error:unhandled': { error: Error; context: string };
}

const serverEvents = new TypedEmitter<ServerEvents>();

// Koa 中间件
function eventTracking(bus: TypedEmitter<ServerEvents>): Koa.Middleware {
  return async (ctx, next) => {
    const start = Date.now();
    bus.emit('request:received', {
      method: ctx.method,
      path: ctx.path,
      ip: ctx.ip
    });

    try {
      await next();
    } catch (err) {
      bus.emit('error:unhandled', { error: err as Error, context: ctx.path });
      throw err;
    } finally {
      bus.emit('request:completed', {
        method: ctx.method,
        path: ctx.path,
        status: ctx.status,
        durationMs: Date.now() - start
      });
    }
  };
}

// 监听事件(比如接 Prometheus 指标)
serverEvents.on('request:completed', ({ method, path, status, durationMs }) => {
  if (durationMs > 1000) {
    console.warn(`[SLOW] ${method} ${path} - ${status} (${durationMs}ms)`);
  }
});

性能考量

TypeScript 高级类型在编译时计算,不影响运行时性能。但递归类型有深度限制(约 50 层),对日常业务足够。

如果遇到 "Type instantiation is excessively deep and possibly infinite" 错误,可以设置递归上限:

type BuildTuple<N extends number, T extends any[] = []> =
  T extends { length: N } ? T : BuildTuple<N, [...T, any]];

type DeepReadonly<T, Depth extends number = 10, Current extends any[] = []> =
  Current['length'] extends Depth
    ? T  // 达到深度限制,停止递归
    : T extends (...args: any[]) => any ? T
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K], Depth, [...Current, any]> }
      : T;

总结

  • DeepReadonly:保护深层嵌套配置对象不被意外修改,适合全局配置、缓存数据
  • DeepPartial:实现类型安全的部分覆盖/合并,适合配置初始化、API 更新 PATCH
  • TypedEmitter:零运行时开销的类型安全事件系统,替换项目中的 any 满天飞的 EventEmitter

这些类型工具的共同点:把运行时可能发生的错误提前到编译期捕获。这才是 TypeScript 真正的价值——不是给 JS 加类型注解,而是用类型系统编码业务约束。

0 评论

评论区

登录 后参与评论