如何从TypeScript动态类型模型推导关联接口?
解决方案
可以通过扩展TypeScript工具类型和模型生成逻辑,实现无需重复定义接口的动态类型推导,同时支持命名风格转换与类型安全的导出方法。以下是具体实现步骤:
1. 定义命名风格转换工具类型
先实现蛇形与小驼峰命名的双向类型映射:
// 蛇形转小驼峰 type SnakeToCamel<S extends string> = S extends `${infer P}_${infer R}` ? `${Lowercase<P>}${Capitalize<SnakeToCamel<R>>}` : Lowercase<S>; // 小驼峰转蛇形 type CamelToSnake<S extends string> = S extends `${infer P}${infer R}` ? `${P extends Capitalize<P> ? "_" : ""}${Lowercase<P>}${CamelToSnake<R>}` : S;
2. 实现扁平对象的动态类型推导
基于模型属性配置,自动提取原始值类型并支持命名风格转换:
// 提取属性对应的原始值类型(处理Date、嵌套模型、数组等场景) type ExtractPrimitiveType<T> = T extends typeof String ? string : T extends typeof Number ? number : T extends typeof Boolean ? boolean : T extends typeof Date ? string : // 导出时Date转为ISO字符串 T extends typeof Array<infer U> ? ExtractPrimitiveType<U>[] : T extends (...args: any[]) => infer R ? ExtractPrimitiveType<R> : T extends { new(): infer Instance } ? FlattenedObject<Instance> : never; // 基础扁平对象类型(保持原命名风格) type FlattenedObject<T> = T extends { __attributes: infer Attrs } ? { [K in keyof Attrs]: ExtractPrimitiveType<Attrs[K]> } : never; // 支持自定义命名风格的扁平对象类型 type CasedFlattenedObject<T, CaseTransform extends (key: string) => string> = T extends { __attributes: infer Attrs } ? { [K in keyof Attrs as CaseTransform extends ((k: string) => infer R) ? R : never]: ExtractPrimitiveType<Attrs[K]> } : never; // 预定义蛇形命名的扁平对象类型 type SnakeCasedFlattenedObject<T> = CasedFlattenedObject<T, CamelToSnake>; // 预定义小驼峰命名的扁平对象类型 type CamelCasedFlattenedObject<T> = CasedFlattenedObject<T, SnakeToCamel>;
3. 扩展模型生成函数,添加类型安全的转换方法
修改ModelFromAttributes,给模型类注入导出方法,确保返回值类型自动推导:
function ModelFromAttributes<Attrs extends Record<string, any>>(attrs: Attrs) { return class BaseModel { // 保存属性配置,用于类型推导 static __attributes = attrs; __attributes = attrs; constructor(data: Partial<{ [K in keyof Attrs]: any }>) { // 原有的实例化逻辑:将输入数据转换为对应类型 Object.keys(attrs).forEach(key => { const type = attrs[key]; this[key as keyof Attrs] = typeof type === 'function' ? type(data[key]) : Array.isArray(type) ? data[key]?.map(item => type[0](item)) : new type(data[key]); }); } // 导出为原始扁平对象(保持原命名风格) toFlattenedObject(): FlattenedObject<this> { const result: Record<string, any> = {}; Object.keys(this.__attributes).forEach(key => { const value = this[key as keyof Attrs]; if (value instanceof Date) { result[key] = value.toISOString(); } else if (Array.isArray(value)) { result[key] = value.map(item => item.toFlattenedObject?.() ?? item); } else if (value instanceof BaseModel) { result[key] = value.toFlattenedObject(); } else { result[key] = value; } }); return result as FlattenedObject<this>; } // 导出为指定命名风格的扁平对象 toCasedFlattenedObject<CaseFn extends (key: string) => string>( caseTransform: CaseFn ): CasedFlattenedObject<this, CaseFn> { const result: Record<string, any> = {}; Object.keys(this.__attributes).forEach(key => { const casedKey = caseTransform(key); const value = this[key as keyof Attrs]; if (value instanceof Date) { result[casedKey] = value.toISOString(); } else if (Array.isArray(value)) { result[casedKey] = value.map(item => item.toFlattenedObject?.() ?? item); } else if (value instanceof BaseModel) { result[casedKey] = value.toFlattenedObject(); } else { result[casedKey] = value; } }); return result as CasedFlattenedObject<this, CaseFn>; } }; }
4. 使用示例
// 定义嵌套模型 class Pet extends ModelFromAttributes({ name: String, age: Number }) {} // 定义主模型 class Person extends ModelFromAttributes({ joined: Date, name: String, first_name: String, last_name: String, age: Number, skills: [String], locale: (v: any) => (v === 'vacation' ? null : String), pets: [Pet], }) {} // 实例化模型 const person = new Person({ joined: new Date(), name: "John", first_name: "John", last_name: "Doe", age: 30, skills: ["TypeScript", "JavaScript"], locale: "work", pets: [{ name: "Buddy", age: 5 }], }); // 导出原风格扁平对象(类型自动推导为FlattenedObject<Person>) const flatObj = person.toFlattenedObject(); // 导出小驼峰风格扁平对象(类型自动推导为CamelCasedFlattenedObject<Person>) const camelObj = person.toCasedFlattenedObject((key) => key.replace(/_([a-z])/g, (_, c) => c.toUpperCase()) ); // 导出蛇形风格扁平对象(类型自动推导为SnakeCasedFlattenedObject<Person>) const snakeObj = person.toCasedFlattenedObject((key) => key.replace(/([A-Z])/g, "_$1").toLowerCase() );
该方案实现了:
- 无需手动编写
FlattenedObject等接口,完全通过类型推导生成 - 转换方法的返回值具备完整的智能提示与类型检查
- 支持自定义命名风格转换,同时兼容嵌套模型与复杂属性类型
内容的提问来源于stack exchange,提问作者c.m
相关产品推荐
相关产品推荐

