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
DeepPartial 是 DeepReadonly 的镜像——让嵌套对象的所有属性变为可选:
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 评论
评论区
登录 后参与评论