如何从TypeScript容器中获取正确类型的对象?
我实现了一个用来管理各类服务(比如Config、Log、Translator等)的容器,代码如下:
export class Container { private readonly services: Map<string, IService> = new Map(); // ... add(name: string): void { if (this.services.has(name)) { return; } const source = require(`./services/${name}/${_.startCase(name)}`) as ConstructorFunction; const conf = (this.services.get('conf') as Config).get('services.' + name); this.services.set(name, new source(conf)); } get(name: string): ??? { if (!this.services.has(name)) { this.add(name); } return this.services.get(name); } } interface IService { readonly handle: string; readonly dependencies: string[]; readonly tags: string[]; } type ConstructorFunction = new (...args: any[]) => {};
Config已经在容器的构造函数里手动初始化了(这部分代码省略)。所有服务都实现了IService接口,但彼此之间没有共同的方法。现在从容器里取服务的时候,必须手动断言类型(比如this.services.get('conf') as Config)才能让编译器识别具体类型,这实在太繁琐了。
我想知道:
- 该如何重写
get方法(可能需要同时修改add方法),让它能返回对应服务的正确具体类型? - 目前
services: Map<string, IService>这个类型定义是否恰当?
解决方案:用类型映射+泛型实现类型安全的服务容器
要解决这个问题,核心是让TypeScript明确服务名称和其具体类型的对应关系,我们可以通过类型映射(Type Map)和泛型方法来实现完全类型安全的容器。
1. 定义服务类型映射
首先,创建一个类型来记录每个服务名称对应的具体类型,相当于给容器建立一份"类型清单":
// 提前导入所有服务的类型(根据你的文件路径调整) import type { Config } from './services/config/Config'; import type { Log } from './services/log/Log'; import type { Translator } from './services/translator/Translator'; // 定义服务名称到具体类型的映射 type ServiceMap = { conf: Config; log: Log; translator: Translator; };
2. 改造Container类的内部存储类型
调整容器的services Map类型,让它基于上面的ServiceMap,而不是宽泛的IService:
export class Container { // 现在Map的键只能是ServiceMap中定义的服务名,值是对应服务的具体类型(同时保留IService约束) private readonly services: Map<keyof ServiceMap, IService & ServiceMap[keyof ServiceMap]> = new Map(); // ... }
这里IService & ServiceMap[keyof ServiceMap]既确保每个服务都符合IService接口的公共约束,又让TypeScript能追踪每个键对应的具体类型。
3. 重写泛型的get方法
把get方法改成泛型方法,让它根据传入的服务名称自动返回对应的具体类型:
get<T extends keyof ServiceMap>(name: T): ServiceMap[T] { if (!this.services.has(name)) { this.add(name); } // 因为已经通过泛型约束了name的范围,这里可以安全断言类型 return this.services.get(name) as ServiceMap[T]; }
4. 调整add方法的类型约束
为了确保add方法只能接受ServiceMap中定义的合法服务名,同时让构造函数的类型匹配对应服务:
add<T extends keyof ServiceMap>(name: T): void { if (this.services.has(name)) { return; } // 调整导入的类型断言,确保构造函数对应正确的服务类型 // 注意:如果你的服务是默认导出就加.default,命名导出就用对应的导出名 const ServiceClass = require(`./services/${name}/${_.startCase(name)}`).default as new (conf: any) => ServiceMap[T]; // 现在get('conf')会自动返回Config类型,不需要手动断言了! const conf = this.get('conf').get(`services.${name}`); this.services.set(name, new ServiceClass(conf)); }
5. 初始化Config服务(补充构造函数)
因为你提到Config是手动初始化的,在构造函数里完成这一步时,确保类型正确:
constructor() { // 根据你的Config构造逻辑调整参数 const configInstance = new Config(); this.services.set('conf', configInstance); }
关于services: Map<string, IService>的类型定义是否恰当?
原来的定义不够恰当,因为它只告诉TypeScript所有值都是IService类型,但完全丢失了每个服务的具体类型信息,导致每次取出服务都需要手动断言,既繁琐又容易出错。
而我们改造后的Map<keyof ServiceMap, IService & ServiceMap[keyof ServiceMap]>,既保留了IService的公共约束,又让TypeScript能精准追踪每个键对应的具体类型,是更类型安全、更符合TypeScript设计理念的选择。
使用示例
现在你可以直接调用get方法,编译器会自动推断出正确的类型,不需要任何手动断言:
const container = new Container(); const config = container.get('conf'); // 自动推断为Config类型,可直接调用Config的所有方法 const logger = container.get('log'); // 自动推断为Log类型 const translator = container.get('translator'); // 自动推断为Translator类型
内容的提问来源于stack exchange,提问作者Forseti

