TypeScript中定义子类映射并运行时创建子类的优雅实现
这是个很典型的TypeScript类型系统问题,咱们先搞清楚为什么会报错,再给你几个优雅的解决方案。
问题根源
你用typeof A作为映射对象的类型时,typeof A代表的是抽象类A的构造函数类型。TypeScript的类型系统会严格禁止实例化抽象类——哪怕你实际存入的都是A的非抽象子类,类型系统没法确定这一点,所以会抛出“Cannot create an instance of abstract class”的编译错误。
而用typeof B | typeof C虽然可行,但子类一多就会变得无比冗长,完全不符合可维护性要求。
优雅解决方案:定义非抽象构造函数类型
核心思路是定义一个专门的类型,用来描述「可以实例化出A类实例的非抽象构造函数」,替代typeof A。
方案1:固定构造参数的构造函数类型
如果A的构造函数参数是固定的,直接明确定义构造函数类型:
abstract class A { public name: string; constructor(name: string) { this.name = name; } abstract getName(): string; } class B extends A { getName(): string { return "B: " + this.name; } } class C extends A { getName(): string { return "C: " + this.name; } } // 定义能创建A实例的非抽象构造函数类型 type AConstructor = new (name: string) => A; // 映射对象使用这个类型 const classes: { [type: string]: AConstructor } = { b: B, c: C }; function test(type: string, name: string) { if (!classes.hasOwnProperty(type)) { throw new Error(`Invalid type: ${type}`); } const _class = classes[type]; return new _class(name); // 编译完全正常 }
这个类型AConstructor明确告诉TypeScript:映射里的每个值都是可以被new、且返回A实例的构造函数,B和C的构造函数完全符合这个要求,所以不会再报错。后续新增A的子类时,只需要把它加到classes对象里,类型定义完全不需要修改。
方案2:通用构造函数类型(适配构造参数变化)
如果A的构造函数参数可能会变动,或者想让类型更通用,可以用TypeScript内置的ConstructorParameters和InstanceType工具类型来自动推导:
// 自动提取A的构造参数和实例类型 type AConstructor = new (...args: ConstructorParameters<typeof A>) => InstanceType<typeof A>; const classes: { [type: string]: AConstructor } = { b: B, c: C }; // 后续test函数写法和之前一致 function test(type: string, name: string) { if (!classes.hasOwnProperty(type)) { throw new Error(`Invalid type: ${type}`); } const _class = classes[type]; return new _class(name); }
这种方式的好处是,如果后续修改A的构造函数参数,AConstructor会自动同步更新,不需要手动调整类型定义。
进阶:让返回类型更精确
如果你希望test函数返回的实例类型是具体的子类(比如调用test('b', 'xxx')返回B类型,而不是A类型),可以用类型字面量约束type参数:
// 定义允许的类型字面量 type ValidClassType = 'b' | 'c'; // 使用Record来约束映射对象的键 const classes: Record<ValidClassType, AConstructor> = { b: B, c: C }; // 泛型函数推导返回类型 function test<T extends ValidClassType>(type: T, name: string): InstanceType<typeof classes[T]> { const _class = classes[type]; return new _class(name) as InstanceType<typeof classes[T]>; } // 调用时会自动推导类型 const bInstance = test('b', 'test'); // bInstance的类型是B const cInstance = test('c', 'test'); // cInstance的类型是C
这种方式兼顾了类型安全和返回类型的精确性,适合对类型要求较高的场景。
总结
不管用哪种方案,核心都是用一个明确的非抽象构造函数类型替代typeof A,既解决了编译错误,又避免了冗长的联合类型定义,后续扩展子类也非常方便。
内容的提问来源于stack exchange,提问作者Rocco

