如何用TypeScript模板字面量类型实现对象键方法联合枚举与类型安全调用
实现类型安全的动态方法调用
需求背景
需要将rootStore中所有存储对象的键与对应方法名组合成联合类型(如'aStore.aMethod1'、'bStore.bMethod3'等),同时让dynamicCall函数具备类型安全性:
- 限制
action参数只能是合法的存储对象+方法名组合 - 确保传入的参数类型与目标方法的参数类型完全匹配
解决方案
1. 定义合法的Action联合类型
通过模板字面量类型和映射类型遍历rootStore的结构,自动生成所有合法的store.method组合:
type ActionName<T> = { [StoreKey in keyof T]: `${StoreKey & string}.${keyof T[StoreKey] & string}` }[keyof T];
这个类型会:
- 遍历
T的每个存储键(如aStore、bStore) - 对每个存储对象,遍历其所有方法键
- 将存储键与方法键拼接成
store.method格式的字符串 - 最终生成所有合法组合的联合类型
2. 提取方法的参数类型
通过条件类型和类型推断,根据传入的action字符串,自动提取对应方法的参数类型:
type GetMethodParams<T, A extends ActionName<T>> = A extends `${infer StoreKey}.${infer MethodKey}` ? StoreKey extends keyof T ? MethodKey extends keyof T[StoreKey] ? T[StoreKey][MethodKey] extends (...args: infer P) => any ? P : never : never : never : never;
该类型会逐层解析action字符串:
- 拆分出存储键
StoreKey和方法键MethodKey - 校验
StoreKey和MethodKey的合法性 - 提取目标方法的参数类型数组
3. 改造dynamicCall函数
利用上述类型,让函数的参数类型完全受目标方法约束,同时优化内部类型断言:
function dynamicCall<T extends Record<string, Record<string, (...args: any[]) => any>>>( action: ActionName<T>, ...args: GetMethodParams<T, typeof action> ) { const [storeName, method] = action.split('.') as [keyof T, keyof T[keyof T]]; const store = rootStore[storeName]; if (store && method in store) { (store[method as keyof typeof store] as (...args: any[]) => any).apply(undefined, args); } }
完整代码示例
const objA = { aMethod1: function (_params: string) { console.log("obj A a method1") return 'A1' }, aMethod2: function (_params: string, _param2: string) { console.log("obj A a method2") return 'A2' } } const objB = { bMethod1: function (_params: string) { console.log("obj B bmethod1") return 'B1' }, bMethod2: function (_params: string, _param2: string) { console.log("obj B bmethod2") return 'B2' }, bMethod3: function (_params: string, _param2: string) { console.log("obj B bmethod3") return 'B3'; } } const rootStore = { aStore: objA, bStore: objB, } // 生成合法的action联合类型 type ActionName<T> = { [StoreKey in keyof T]: `${StoreKey & string}.${keyof T[StoreKey] & string}` }[keyof T]; // 根据action提取对应方法的参数类型 type GetMethodParams<T, A extends ActionName<T>> = A extends `${infer StoreKey}.${infer MethodKey}` ? StoreKey extends keyof T ? MethodKey extends keyof T[StoreKey] ? T[StoreKey][MethodKey] extends (...args: infer P) => any ? P : never : never : never : never; // 类型安全的动态调用函数 function dynamicCall<T extends Record<string, Record<string, (...args: any[]) => any>>>( action: ActionName<T>, ...args: GetMethodParams<T, typeof action> ) { const [storeName, method] = action.split('.') as [keyof T, keyof T[keyof T]]; const store = rootStore[storeName]; if (store && method in store) { (store[method as keyof typeof store] as (...args: any[]) => any).apply(undefined, args); } } // 合法调用示例 dynamicCall<typeof rootStore>('aStore.aMethod1', 'test'); // ✅ 参数符合aMethod1要求 dynamicCall<typeof rootStore>('bStore.bMethod2', 'foo', 'bar'); // ✅ 参数符合bMethod2要求 // 错误调用示例(TypeScript会报错) // dynamicCall<typeof rootStore>('aStore.invalidMethod'); // ❌ 方法不存在 // dynamicCall<typeof rootStore>('aStore.aMethod1', 123); // ❌ 参数类型不匹配 // dynamicCall<typeof rootStore>('bStore.bMethod3', 'onlyOneParam'); // ❌ 参数数量不足
内容的提问来源于stack exchange,提问作者Mr.Trieu
相关产品推荐
相关产品推荐

