接受继承类型的函数的类型

时间:2019-05-11 16:49:49

标签: typescript

假设配置对象的以下继承结构

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的等效对象。有这样的事吗?

2 个答案:

答案 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类型以及相关的“类型”,例如ATypeBType

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) })