基于参数类型的类方法返回类型

时间:2020-11-01 22:58:24

标签: typescript conditional-types

我有一个函数,可以接受任何东西作为参数,但是如果它接收到function(带有原型),则它应该返回该函数(或类的实例),因为它们是函数)。 如果没有,它可能会在地图中看起来像分配给该键的任何东西,然后将其返回。

超级简化的代码看起来像(playground):

// Go check service.get() method
const isConstructable = (fn: any) => typeof fn === 'function' && 'prototype' in fn;

class Animal {
  name: string | undefined
  constructor(name?: string){
    this.name = name;
  }
}

const service = new (class Service {
  map = new Map()

  set(key: any, value: any = key) {
    return this.map.set(key, value)
  }
  
  get<T>(key: { new (): T }): T {
    const ToResolve = this.map.get(key);
    if (isConstructable(ToResolve)) {
        return new ToResolve();
    }
    
    return ToResolve;
  }
})

service.set('bob', new Animal('Bob'))
service.set(Animal);

const genericAnimal = service.get(Animal)
const bob = service.get('bob') // Argument of type 'string' is not assignable to parameter of type 'new () => unknown'

console.log(genericAnimal instanceof Animal)
console.log(bob instanceof Animal)

我正在阅读Using Class Types in Generics并找到了这段代码

function create<T>(c: { new (): T }): T {
  return new c();
}

它运作良好,但是我需要get(key)不仅接受构造函数,而且接受Map接受的任何原语,例如:

const jane = {} // object reference, since TS won't let me use symbols
service.set(jane, class Jane extends Animal {
  constructor() {
    super('Jane')
  }
})
console.log(service.get(jane).name === 'Jane')

这样,我会遇到类似Argument of type 'string' is not assignable to parameter of type 'new () => unknown'的编译错误

还尝试了:get<T>(key: T): T extends Function ? T : unknown,虽然显然不起作用,但感觉非常接近:(

是否需要为key参数创建特定类型?定义重载?怎么样?

预先感谢

1 个答案:

答案 0 :(得分:2)

我认为您对get的签名非常接近。如果要使用条件类型,可以这样写:

  get<T>(key: T): T extends new () => infer I ? I : unknown;
  get(key: any) {
    const ToResolve = this.map.get(key);
    if (isConstructable(ToResolve)) {
      return new ToResolve();
    }
    return ToResolve;
  }

在这种情况下,如果类型T对应于零参数构造函数,则返回类型将是该构造函数的实例类型。否则,返回类型为unknown。请注意,编译器无法验证实现是否与该调用签名相对应(有关更多信息,请参见microsoft/TypeScript#33912),因此我使用的是实现较为宽松的单调用签名overload。这样编译就可以了,但是需要注意的是,您有责任确保实现符合呼叫签名。


这对于您的第一组示例使用是理想的:

const genericAnimal = service.get(Animal) // Animal
const bob = service.get('bob') // unknown

但是当然,set()方法的类型太松散,以至于不能保证它会被正确使用。

service.set(Animal, 1234); // no error? oops.

我建议更改set()方法以反映get()方法中发生的事情:

  set<T>(
    key: T, 
    ...args: T extends new () => infer I ? [value?: I] : 
      [value: unknown]
  ): void;
  set(key: any, value?: any) {
    this.map.set(key, typeof value === "undefined" ? key : value);
  }

该签名可能看起来很吓人,但基本上是说,如果key是构造函数,则可以使用零或一个附加参数(类型为{的参数元组)调用set() {1}},其中[value?: I]是类实例类型,但是如果I不是构造函数,则必须使用附加参数(类型为{的参数元组)调用key {1}}。

然后,如果有人尝试通过构造函数传递一些奇怪的信息,则会出现编译器错误:

set()

最后,我真的不确定如何跟踪为非构造函数键输入的值的类型。编译器将其视为[value: unknown],这意味着如果您设置并获得它,则可能会丢失信息:

service.set(Animal, 1234); // error!
service.set(Animal); // okay
service.set('bob', new Animal('Bob')) // okay

您将需要使用type assertionstype guard来解决此问题:

unknown

可能还有其他方法可以使用assertion functionsfluent API来跟踪类型,但这似乎超出了问题的范围。


Playground link to code