TypeScript中如何限制build方法仅接收Plant抽象类的非抽象子类作为参数并实例化
嘿,这个问题其实不用靠运行时判断来解决!TypeScript本身就支持在类型层面限制build方法只能接收可实例化的Plant子类,从编译阶段就把错误拦下来,比运行时判断更优雅。
问题根源
你现在的build方法参数类型是typeof Plant,这个类型包含了抽象类Plant本身——而抽象类的构造函数是不能被直接new的,所以当你传入Plant(虽然你现在没这么做,但类型上允许)或者不小心定义了另一个抽象子类时,就会触发报错。
类型层面的解决方案:用构造签名限制
我们可以把build方法的参数类型改成构造签名,明确要求传入的类必须是能通过new (cell: Cell)创建实例的非抽象类:
class Cell { // 修改参数类型为构造签名 build(c: new (cell: Cell) => Plant) { if (c.cost <= game.money) { this.plant = new c(this); // 现在不会再报抽象类实例化的错误了! game.money -= c.cost; } } }
这个new (cell: Cell) => Plant的意思是:
- 这是一个构造函数,接收一个
Cell类型的参数 - 调用它会返回一个
Plant类型的实例
而抽象类的构造函数不符合这个签名,因为TypeScript会明确禁止抽象类被直接实例化,所以如果有人尝试把Plant本身传给build,编译阶段就会报错:
Cannot assign an abstract constructor type to a non-abstract constructor type.
完美实现了你想要的“仅接收继承自Plant的非抽象类”的限制!
优化你的植物类代码
另外,你的Sunflower类可以简化一下,父类已经声明了cell属性,子类不需要重复定义;如果构造函数只是调用super,也可以省略:
class Sunflower extends Plant { public static dmg = 0; public static cost = 50; // 不需要重复声明cell,也不需要写constructor,默认会调用super(cell) }
可选的运行时兜底(非必需)
如果你担心极端情况(比如绕过TypeScript类型检查的JS代码调用),可以加个简单的运行时判断兜底,比如检查实例是否是Plant的实例:
build(c: new (cell: Cell) => Plant) { if (c.cost <= game.money) { const newPlant = new c(this); if (newPlant instanceof Plant) { this.plant = newPlant; game.money -= c.cost; } } }
不过这个兜底在正常的TypeScript开发中几乎用不上,因为类型检查已经把问题解决了。
完整修改后的代码
abstract class Plant { public static dmg: number; public static cost: number; constructor(public cell: Cell) {} // 用public参数简化属性声明 } class Sunflower extends Plant { public static dmg = 0; public static cost = 50; } class Cell { plant?: Plant; build(c: new (cell: Cell) => Plant) { // 假设game是全局可用的变量 if (c.cost <= game.money) { this.plant = new c(this); game.money -= c.cost; } } } // 测试调用:完全没问题 let c = new Cell(); c.build(Sunflower); // 如果尝试传Plant本身,编译阶段就会报错 // c.build(Plant); // 报错:Cannot assign an abstract constructor type to a non-abstract constructor type.
这样既满足了“新增植物类型只需配置静态属性”的需求,又从根源上避免了抽象类实例化的错误,而且类型安全!
内容的提问来源于stack exchange,提问作者0brine

