如何实现TypeScript中模块依赖deps数组的点表示法类型安全?
实现TypeScript中deps数组的严格类型检查
需求是让createModules配置中的deps数组仅接受${模块名}.${模块服务名}格式的合法字符串,当前错误的字符串(比如data.serviceDoesNotExist)能通过类型检查,原因是TS将其推断为模块名.${string}的宽泛类型,无法精确校验服务名称。
解决方案
通过调整类型定义,让TypeScript能精准捕获每个模块对应的服务名称,实现严格的类型约束:
- 定义辅助类型提取模块服务
首先创建一个类型,用来从模块的factory函数中提取其返回的服务键名:
type ModuleServices<T> = T extends () => Promise<(...args: any[]) => { services: infer S }> ? keyof S : never;
- 重构Config类型
修改Config类型,针对每个模块单独关联其对应的服务名称,而不是混合所有模块的服务:
export type Config<M> = { [K in keyof M]: { deps?: Array<`${Extract<K, string>}.${ModuleServices<M[K]['factory']> & string}`>; factory: M[K]['factory']; }; };
- 约束createModules的泛型参数
确保泛型M符合模块配置的结构,让TS能正确推断每个模块的类型:
function createModules<M extends Record<string, { factory: () => Promise<(...args: any[]) => { services?: Record<string, any> }> }>>(config: Config<M>) {}
完整示例代码
createModules({ data: { factory: () => Promise.resolve(() => { return { services: { createResource: () => {} }, }; }) }, dependentModule: { deps: ['data.createResource'], // ✅ 合法,通过检查 factory: () => Promise.resolve(() => ({})) }, incorrectConfig: { deps: ['data.serviceDoesNotExist'], // ❌ 非法,TS会抛出错误 factory: () => Promise.resolve(() => ({})) } }); type ModuleServices<T> = T extends () => Promise<(...args: any[]) => { services: infer S }> ? keyof S : never; export type Config<M> = { [K in keyof M]: { deps?: Array<`${Extract<K, string>}.${ModuleServices<M[K]['factory']> & string}`>; factory: M[K]['factory']; }; }; function createModules<M extends Record<string, { factory: () => Promise<(...args: any[]) => { services?: Record<string, any> }> }>>(config: Config<M>) {}
原理说明
原来的类型定义中,M[keyof M]['factory']取的是所有模块factory的联合类型,导致服务名也变成了所有模块服务的联合,TS无法精确对应到单个模块的合法服务。重构后,在映射类型中针对每个模块K,单独提取该模块factory返回的服务名称,让deps的类型成为该模块能引用的合法模块名.服务名字符串字面量联合,从而实现严格的类型校验。
内容的提问来源于stack exchange,提问作者Andy Jessop
相关产品推荐
相关产品推荐

