假设配置对象的以下继承结构
class ConfBase{
constructor(public baseVal:any){}
}
class AConf extends ConfBase{
constructor(public a: any) {
super("A")
}
}
class BConf extends ConfBase{
constructor(public b: any) {
super("B")
}
}
具有相应的服务结构
class ServiceBase<T>{
constructor(conf: ConfBase) {
//...
}
}
type AType = {/* ... */}
class AService extends ServiceBase<AType>{
constructor(conf: AConf) {
super(conf)
//...
}
}
type BType = {/* ... */}
class BService extends ServiceBase<BType>{
constructor(conf: BConf) {
super(conf)
//...
}
}
现在,我想创建一个包含配置对象并创建相应服务对象的功能映射。
let serviceFactory:Map<string, (conf: ConfBase) => ServiceBase<any>> = new Map()
serviceFactory.set('A', (aConf: AConf) => { return new AService(aConf) })
serviceFactory.set('B', (bConf: BConf) => { return new BService(bConf) })
但是,将TSC配置为使用strictFunctionTypes: true
时,会产生以下错误:
类型'(aConf:AConf)=> AService'的参数不可分配给 '(conf:ConfBase)=> ServiceBase'类型的参数。
参数'aConf'和'conf'的类型不兼容。 类型'ConfBase'中缺少属性'a',但类型'AConf'中必需。
有没有办法使类型系统与我的想法一致?肮脏的解决方法是仅将config对象定义为conf:any
,但我宁愿使用Java的? extends ConfBase
的等效对象。有这样的事吗?
答案 0 :(得分:1)
您似乎对阻止TypeScript这样做并不是很感兴趣:
const aFactory = serviceFactory.get("A");
if (aFactory) {
aFactory(new BConf("oopsie")); // no error
}
给定serviceFactory
的类型,该代码将正确编译,然后在运行时给出奇怪的结果。另外,即使您传入了正确的配置,返回类型也为ServiceBase<any>
,而不是ServiceBase<AType>
,因此您将有责任自己声明该类型。
如果您对此感到满意,那么让编译器默认的方式是:
serviceFactory.set('A', (aConf: ConfBase) => { return new AService(aConf as AConf) })
serviceFactory.set('B', (bConf: ConfBase) => { return new BService(bConf as BConf) })
也就是说,您具有接受任何ConfBase
的函数,然后只是告诉编译器,在运行时仅将AConf
用于第一个,并将BConf
用于对第二个。
如果您更关心类型安全,则需要通过告诉编译器有关传递到serviceFactory
的名称与所构造的特定服务之间的关系,来显着地重构此代码。
这是前进的一种可能方法。让我们保留这部分不变:
class ConfBase {
constructor(public baseVal: string) { }
}
class AConf extends ConfBase {
constructor(public a: any) {
super("A")
}
}
class BConf extends ConfBase {
constructor(public b: any) {
super("B")
}
}
然后对于服务,我们需要使它们通用,并引用相应的ConfBase
类型以及相关的“类型”,例如AType
或BType
。
abstract class ServiceBase<C extends ConfBase, T> {
conf: C
constructor(conf: C) {
this.conf = conf;
}
// shouldn't ServiceBase<T> do something with T?
abstract getValueOfTypeOrSomething(): T;
}
type AType = { a: string } // for example
class AService extends ServiceBase<AConf, AType>{
constructor(conf: AConf) {
super(conf)
}
getValueOfTypeOrSomething() {
return { a: "whoKnows" };
}
}
type BType = { b: number } // for example
class BService extends ServiceBase<BConf, BType>{
constructor(conf: BConf) {
super(conf)
}
getValueOfTypeOrSomething() {
return { b: 12345 };
}
}
现在,我们可以描述从名称"A"
和"B"
到ServiceMap
的映射:
// describe the mapping from name to service
interface ServiceMap {
A: AService
B: BService
}
并且服务工厂可以是与ServiceMap
相关的映射类型:
let serviceFactory: { [K in keyof ServiceMap]: (conf: ServiceMap[K]['conf']) => ServiceMap[K] } = {
A: (aConf: AConf) => { return new AService(aConf) },
B: (bConf: BConf) => { return new BService(bConf) }
}
Map
并不是很好的表示,因为它的作用就像字典,其中所有条目都是同一类型。但是在这里您要记住,"A"
键键入的条目是AService
制造者。因此,以下内容是类型安全的:
const aFactory = serviceFactory.A;
aFactory(new BConf("oopsie")); // error, as desired
const aService = aFactory(new AConf("yay")); // okay
// aService is known to be an AService
const aType = aService.getValueOfTypeOrSomething();
// aType is known to be an AType
好的,希望能有所帮助。祝你好运!
答案 1 :(得分:0)
type Configs = AConf & BConf;
let serviceFactory:Map<string, (conf: Configs) => ServiceBase<Configs>> = new Map()
serviceFactory.set('A', (aConf: AConf) => { return new AService(aConf) })
serviceFactory.set('B', (bConf: BConf) => { return new BService(bConf) })